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

View File

@@ -10,7 +10,11 @@ import {
setSystemPromptInjection, setSystemPromptInjection,
} from '../context' } from '../context'
import { clearMemoryFileCaches } from '../utils/claudemd' 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 tempDir = ''
let projectClaudeMdContent = '' let projectClaudeMdContent = ''

View File

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

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 // 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 { export function setPromptId(id: string | null): void {
STATE.promptId = id 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') 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 // Empty body or null = no work available
if (!response.data) { if (!response.data) {

View File

@@ -448,9 +448,11 @@ export async function runBridgeLoop(
): (status: SessionDoneStatus) => void { ): (status: SessionDoneStatus) => void {
return (rawStatus: SessionDoneStatus): void => { return (rawStatus: SessionDoneStatus): void => {
const workId = sessionWorkIds.get(sessionId) const workId = sessionWorkIds.get(sessionId)
rcLog(`session done: sessionId=${sessionId} workId=${workId ?? 'none'} status=${rawStatus}` + rcLog(
` wasTimedOut=${timedOutSessions.has(sessionId)} duration=${Math.round((Date.now() - startTime) / 1000)}s` + `session done: sessionId=${sessionId} workId=${workId ?? 'none'} status=${rawStatus}` +
` stderr=${handle.lastStderr.length > 0 ? handle.lastStderr.join('\\n').slice(0, 500) : '(none)'}`) ` 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) activeSessions.delete(sessionId)
sessionStartTimes.delete(sessionId) sessionStartTimes.delete(sessionId)
sessionWorkIds.delete(sessionId) sessionWorkIds.delete(sessionId)
@@ -609,7 +611,9 @@ export async function runBridgeLoop(
const pollConfig = getPollIntervalConfig() const pollConfig = getPollIntervalConfig()
try { try {
rcLog(`poll: envId=${environmentId} activeSessions=${activeSessions.size}`) rcLog(
`poll: envId=${environmentId} activeSessions=${activeSessions.size}`,
)
const work = await api.pollForWork( const work = await api.pollForWork(
environmentId, environmentId,
environmentSecret, environmentSecret,
@@ -864,7 +868,9 @@ export async function runBridgeLoop(
break break
case 'session': { case 'session': {
const sessionId = work.data.id 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 { try {
validateBridgeId(sessionId, 'session_id') validateBridgeId(sessionId, 'session_id')
} catch { } catch {
@@ -1032,9 +1038,9 @@ export async function runBridgeLoop(
rcLog( rcLog(
`spawning session: sessionId=${sessionId} sdkUrl=${sdkUrl}` + `spawning session: sessionId=${sessionId} sdkUrl=${sdkUrl}` +
` useCcrV2=${useCcrV2} workerEpoch=${workerEpoch}` + ` useCcrV2=${useCcrV2} workerEpoch=${workerEpoch}` +
` dir=${sessionDir}` + ` dir=${sessionDir}` +
` accessToken=${secret.session_ingress_token ? secret.session_ingress_token.slice(0, 8) + '...' : 'NONE'}`, ` accessToken=${secret.session_ingress_token ? secret.session_ingress_token.slice(0, 8) + '...' : 'NONE'}`,
) )
const spawnResult = safeSpawn( const spawnResult = safeSpawn(
spawner, spawner,
@@ -1281,8 +1287,8 @@ export async function runBridgeLoop(
const errMsg = describeAxiosError(err) const errMsg = describeAxiosError(err)
rcLog( rcLog(
`poll error: ${errMsg}` + `poll error: ${errMsg}` +
` isConn=${isConnectionError(err)} isServer=${isServerError(err)}` + ` isConn=${isConnectionError(err)} isServer=${isServerError(err)}` +
` activeSessions=${activeSessions.size}`, ` activeSessions=${activeSessions.size}`,
) )
if (isConnectionError(err) || isServerError(err)) { if (isConnectionError(err) || isServerError(err)) {
@@ -1676,7 +1682,7 @@ async function stopWorkWithRetry(
} }
const errMsg = errorMessage(err) const errMsg = errorMessage(err)
if (attempt < MAX_ATTEMPTS) { if (attempt < MAX_ATTEMPTS) {
const delay = addJitter(baseDelayMs * Math.pow(2, attempt - 1)) const delay = addJitter(baseDelayMs * 2 ** (attempt - 1))
logger.logVerbose( logger.logVerbose(
`Failed to stop work ${workId} (attempt ${attempt}/${MAX_ATTEMPTS}), retrying in ${formatDelay(delay)}: ${errMsg}`, `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 - 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 - Run \`claude\` first in the directory to accept the workspace trust dialog
${serverNote}` ${serverNote}`
// biome-ignore lint/suspicious/noConsole: intentional help output
console.log(help) console.log(help)
} }
@@ -2003,7 +2008,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
return return
} }
if (parsed.error) { if (parsed.error) {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error(`Error: ${parsed.error}`) console.error(`Error: ${parsed.error}`)
// eslint-disable-next-line custom-rules/no-process-exit // eslint-disable-next-line custom-rules/no-process-exit
process.exit(1) process.exit(1)
@@ -2042,7 +2046,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
const { PERMISSION_MODES } = await import('../types/permissions.js') const { PERMISSION_MODES } = await import('../types/permissions.js')
const valid: readonly string[] = PERMISSION_MODES const valid: readonly string[] = PERMISSION_MODES
if (!valid.includes(permissionMode)) { if (!valid.includes(permissionMode)) {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error( console.error(
`Error: Invalid permission mode '${permissionMode}'. Valid modes: ${valid.join(', ')}`, `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()]), Promise.all([shutdown1PEventLogging(), shutdownDatadog()]),
sleep(500, undefined, { unref: true }), sleep(500, undefined, { unref: true }),
]).catch(() => {}) ]).catch(() => {})
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error( console.error(
'Error: Multi-session Remote Control is not enabled for your account yet.', '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), // 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. // so we must verify trust was previously established by a normal `claude` session.
if (!checkHasTrustDialogAccepted()) { if (!checkHasTrustDialogAccepted()) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error( console.error(
`Error: Workspace not trusted. Please run \`claude\` in ${dir} first to review and accept the workspace trust dialog.`, `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() const bridgeToken = getBridgeAccessToken()
if (!bridgeToken) { if (!bridgeToken) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(BRIDGE_LOGIN_ERROR) console.error(BRIDGE_LOGIN_ERROR)
// eslint-disable-next-line custom-rules/no-process-exit // eslint-disable-next-line custom-rules/no-process-exit
process.exit(1) process.exit(1)
@@ -2138,7 +2138,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
input: process.stdin, input: process.stdin,
output: process.stdout, output: process.stdout,
}) })
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log( 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', '\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) const found = await readBridgePointerAcrossWorktrees(dir)
if (!found) { if (!found) {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error( console.error(
`Error: No recent session found in this directory or its worktrees. Run \`claude remote-control\` to start a new one.`, `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 ageMin = Math.round(pointer.ageMs / 60_000)
const ageStr = ageMin < 60 ? `${ageMin}m` : `${Math.round(ageMin / 60)}h` const ageStr = ageMin < 60 ? `${ageMin}m` : `${Math.round(ageMin / 60)}h`
const fromWt = pointerDir !== dir ? ` from worktree ${pointerDir}` : '' const fromWt = pointerDir !== dir ? ` from worktree ${pointerDir}` : ''
// biome-ignore lint/suspicious/noConsole: intentional info output
console.error( console.error(
`Resuming session ${pointer.sessionId} (${ageStr} ago)${fromWt}\u2026`, `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('localhost') &&
!baseUrl.includes('127.0.0.1') !baseUrl.includes('127.0.0.1')
) { ) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error( console.error(
'Error: Remote Control base URL uses HTTP. Only HTTPS or localhost HTTP is allowed.', '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 ? getCurrentProjectConfig().remoteControlSpawnMode
: undefined : undefined
if (savedSpawnMode === 'worktree' && !worktreeAvailable) { if (savedSpawnMode === 'worktree' && !worktreeAvailable) {
// biome-ignore lint/suspicious/noConsole: intentional warning output
console.error( console.error(
'Warning: Saved spawn mode is worktree but this directory is not a git repository. Falling back to same-dir.', '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, input: process.stdin,
output: process.stdout, output: process.stdout,
}) })
// biome-ignore lint/suspicious/noConsole: intentional dialog output
console.log( 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` + `\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` + `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); // Only reachable via explicit --spawn=worktree (default is same-dir);
// saved worktree pref was already guarded above. // saved worktree pref was already guarded above.
if (spawnMode === 'worktree' && !worktreeAvailable) { if (spawnMode === 'worktree' && !worktreeAvailable) {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error( console.error(
`Error: Worktree mode requires a git repository or WorktreeCreate hooks configured. Use --spawn=session for single-session mode.`, `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 { try {
validateBridgeId(resumeSessionId, 'sessionId') validateBridgeId(resumeSessionId, 'sessionId')
} catch { } catch {
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error( console.error(
`Error: Invalid session ID "${resumeSessionId}". Session IDs must not contain unsafe characters.`, `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') const { clearBridgePointer } = await import('./bridgePointer.js')
await clearBridgePointer(resumePointerDir) await clearBridgePointer(resumePointerDir)
} }
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error( console.error(
`Error: Session ${resumeSessionId} not found. It may have been archived or expired, or your login may have lapsed (run \`claude /login\`).`, `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') const { clearBridgePointer } = await import('./bridgePointer.js')
await clearBridgePointer(resumePointerDir) await clearBridgePointer(resumePointerDir)
} }
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error( console.error(
`Error: Session ${resumeSessionId} has no environment_id. It may never have been attached to a bridge.`, `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, status: err instanceof BridgeFatalError ? err.status : undefined,
}) })
// Registration failures are fatal — print a clean message instead of a stack trace. // Registration failures are fatal — print a clean message instead of a stack trace.
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error( console.error(
err instanceof BridgeFatalError && err.status === 404 err instanceof BridgeFatalError && err.status === 404
? 'Remote Control environments are not available for your account.' ? '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.`, `Bridge resume env mismatch: requested ${reuseEnvironmentId}, backend returned ${environmentId}. Falling back to fresh session.`,
), ),
) )
// biome-ignore lint/suspicious/noConsole: intentional warning output
console.warn( console.warn(
`Warning: Could not resume session ${resumeSessionId} — its environment has expired. Creating a fresh session instead.`, `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') const { clearBridgePointer } = await import('./bridgePointer.js')
await clearBridgePointer(resumePointerDir) await clearBridgePointer(resumePointerDir)
} }
// biome-ignore lint/suspicious/noConsole: intentional error output
console.error( console.error(
isFatal isFatal
? `Error: ${errorMessage(err)}` ? `Error: ${errorMessage(err)}`

View File

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

View File

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

View File

@@ -20,7 +20,10 @@ export function rcLog(msg: string): void {
try { try {
if (!headerWritten) { if (!headerWritten) {
ensureLogDir() 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 headerWritten = true
} }
const ts = new Date().toISOString().slice(11, 23) // HH:mm:ss.SSS 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) { for (const msg of filtered) {
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string) 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) void transport.writeBatch(events)
}, },
sendControlRequest(request: SDKControlRequest) { sendControlRequest(request: SDKControlRequest) {
@@ -844,8 +847,14 @@ export async function initEnvLessBridgeCore(
) )
return return
} }
const event: TransportMessage = { ...request, session_id: sessionId } as TransportMessage const event: TransportMessage = {
if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') { ...request,
session_id: sessionId,
} as TransportMessage
if (
(request as { request?: { subtype?: string } }).request?.subtype ===
'can_use_tool'
) {
transport.reportState('requires_action') transport.reportState('requires_action')
} }
void transport.write(event as StdoutMessage) void transport.write(event as StdoutMessage)
@@ -860,7 +869,10 @@ export async function initEnvLessBridgeCore(
) )
return return
} }
const event: TransportMessage = { ...response, session_id: sessionId } as TransportMessage const event: TransportMessage = {
...response,
session_id: sessionId,
} as TransportMessage
transport.reportState('running') transport.reportState('running')
void transport.write(event as StdoutMessage) void transport.write(event as StdoutMessage)
logForDebugging('[remote-bridge] Sent control_response') logForDebugging('[remote-bridge] Sent control_response')

View File

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

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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. */ /** Patterns that match known secret/token formats. */
const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [ const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
// GitHub tokens (PAT, OAuth, App, Server-to-server) // 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 // 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 // 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 // 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) // 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") // 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 // npm tokens
{ pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' }, { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' },
// Slack tokens // 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). */ /** Maximum content length before truncation (100KB). */

View File

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

View File

@@ -1,25 +1,23 @@
import { feature } from 'bun:bundle' import { feature } from 'bun:bundle';
import React, { useEffect } from 'react' import React, { useEffect } from 'react';
import { useNotifications } from '../context/notifications.js' import { useNotifications } from '../context/notifications.js';
import { Text } from '@anthropic/ink' import { Text } from '@anthropic/ink';
import { getGlobalConfig } from '../utils/config.js' import { getGlobalConfig } from '../utils/config.js';
import { getRainbowColor } from '../utils/thinking.js' import { getRainbowColor } from '../utils/thinking.js';
// Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter // Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load. // 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. // Teaser window: April 1-7, 2026 only. Command stays live forever after.
export function isBuddyTeaserWindow(): boolean { export function isBuddyTeaserWindow(): boolean {
if (process.env.USER_TYPE === 'ant') return true if (process.env.USER_TYPE === 'ant') return true;
const d = new Date() const d = new Date();
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7 return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
} }
export function isBuddyLive(): boolean { export function isBuddyLive(): boolean {
if (process.env.USER_TYPE === 'ant') return true if (process.env.USER_TYPE === 'ant') return true;
const d = new Date() const d = new Date();
return ( return d.getFullYear() > 2026 || (d.getFullYear() === 2026 && d.getMonth() >= 3);
d.getFullYear() > 2026 || (d.getFullYear() === 2026 && d.getMonth() >= 3)
)
} }
function RainbowText({ text }: { text: string }): React.ReactNode { function RainbowText({ text }: { text: string }): React.ReactNode {
@@ -31,37 +29,35 @@ function RainbowText({ text }: { text: string }): React.ReactNode {
</Text> </Text>
))} ))}
</> </>
) );
} }
// Rainbow /buddy teaser shown on startup when no companion hatched yet. // Rainbow /buddy teaser shown on startup when no companion hatched yet.
// Idle presence and reactions are handled by CompanionSprite directly. // Idle presence and reactions are handled by CompanionSprite directly.
export function useBuddyNotification(): void { export function useBuddyNotification(): void {
const { addNotification, removeNotification } = useNotifications() const { addNotification, removeNotification } = useNotifications();
useEffect(() => { useEffect(() => {
if (!feature('BUDDY')) return if (!feature('BUDDY')) return;
const config = getGlobalConfig() const config = getGlobalConfig();
if (config.companion || !isBuddyTeaserWindow()) return if (config.companion || !isBuddyTeaserWindow()) return;
addNotification({ addNotification({
key: 'buddy-teaser', key: 'buddy-teaser',
jsx: <RainbowText text="/buddy" />, jsx: <RainbowText text="/buddy" />,
priority: 'immediate', priority: 'immediate',
timeoutMs: 15_000, timeoutMs: 15_000,
}) });
return () => removeNotification('buddy-teaser') return () => removeNotification('buddy-teaser');
}, [addNotification, removeNotification]) }, [addNotification, removeNotification]);
} }
export function findBuddyTriggerPositions( export function findBuddyTriggerPositions(text: string): Array<{ start: number; end: number }> {
text: string, if (!feature('BUDDY')) return [];
): Array<{ start: number; end: number }> { const triggers: Array<{ start: number; end: number }> = [];
if (!feature('BUDDY')) return [] const re = /\/buddy\b/g;
const triggers: Array<{ start: number; end: number }> = [] let m: RegExpExecArray | null;
const re = /\/buddy\b/g
let m: RegExpExecArray | null
while ((m = re.exec(text)) !== 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 { TmuxEngine } = await import('./bg/engines/tmux.js')
const tmux = new TmuxEngine() const tmux = new TmuxEngine()
if (!(await tmux.available())) { 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 process.exitCode = 1
return return
} }
@@ -301,7 +303,9 @@ export async function handleBgStart(args: string[]): Promise<void> {
console.log(` Engine: ${result.engineUsed}`) console.log(` Engine: ${result.engineUsed}`)
console.log(` Log: ${result.logPath}`) console.log(` Log: ${result.logPath}`)
console.log() 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 status\` to check status.`)
console.log(`Use \`claude daemon kill ${result.sessionName}\` to stop.`) console.log(`Use \`claude daemon kill ${result.sessionName}\` to stop.`)
} catch (e) { } catch (e) {

View File

@@ -1,7 +1,12 @@
import { spawn } from 'child_process' import { spawn } from 'child_process'
import { openSync, closeSync, mkdirSync } from 'fs' import { openSync, closeSync, mkdirSync } from 'fs'
import { dirname } from 'path' 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' import { tailLog } from '../tail.js'
export class DetachedEngine implements BgEngine { 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> { export async function selectEngine(): Promise<import('../engine.js').BgEngine> {
if (process.platform === 'win32') { if (process.platform === 'win32') {

View File

@@ -1,7 +1,12 @@
import { spawnSync } from 'child_process' import { spawnSync } from 'child_process'
import { execFileNoThrow } from '../../../utils/execFileNoThrow.js' import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'
import { quote } from '../../../utils/bash/shellQuote.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 { export class TmuxEngine implements BgEngine {
readonly name = 'tmux' as const readonly name = 'tmux' as const

View File

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

View File

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

View File

@@ -159,7 +159,9 @@ export async function authLogin({
const orgResult = await validateForceLoginOrg() const orgResult = await validateForceLoginOrg()
if (!orgResult.valid) { 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) process.exit(1)
} }
@@ -209,7 +211,9 @@ export async function authLogin({
const orgResult = await validateForceLoginOrg() const orgResult = await validateForceLoginOrg()
if (!orgResult.valid) { 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) process.exit(1)
} }

View File

@@ -3,203 +3,163 @@
* These are dynamically imported only when the corresponding `claude mcp *` command runs. * These are dynamically imported only when the corresponding `claude mcp *` command runs.
*/ */
import { stat } from 'fs/promises' import { stat } from 'fs/promises';
import pMap from 'p-map' import pMap from 'p-map';
import { cwd } from 'process' import { cwd } from 'process';
import React from 'react' import React from 'react';
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js' import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js';
import { wrappedRender as render } from '@anthropic/ink' import { wrappedRender as render } from '@anthropic/ink';
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js' import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js';
import { import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent, logEvent,
} from '../../services/analytics/index.js' } from '../../services/analytics/index.js';
import { import {
clearMcpClientConfig, clearMcpClientConfig,
clearServerTokensFromLocalStorage, clearServerTokensFromLocalStorage,
getMcpClientConfig, getMcpClientConfig,
readClientSecret, readClientSecret,
saveMcpClientSecret, saveMcpClientSecret,
} from '../../services/mcp/auth.js' } from '../../services/mcp/auth.js';
import { import { connectToServer, getMcpServerConnectionBatchSize } from '../../services/mcp/client.js';
connectToServer,
getMcpServerConnectionBatchSize,
} from '../../services/mcp/client.js'
import { import {
addMcpConfig, addMcpConfig,
getAllMcpConfigs, getAllMcpConfigs,
getMcpConfigByName, getMcpConfigByName,
getMcpConfigsByScope, getMcpConfigsByScope,
removeMcpConfig, removeMcpConfig,
} from '../../services/mcp/config.js' } from '../../services/mcp/config.js';
import type { import type { ConfigScope, ScopedMcpServerConfig } from '../../services/mcp/types.js';
ConfigScope, import { describeMcpConfigFilePath, ensureConfigScope, getScopeLabel } from '../../services/mcp/utils.js';
ScopedMcpServerConfig, import { AppStateProvider } from '../../state/AppState.js';
} from '../../services/mcp/types.js' import { getCurrentProjectConfig, getGlobalConfig, saveCurrentProjectConfig } from '../../utils/config.js';
import { import { isFsInaccessible } from '../../utils/errors.js';
describeMcpConfigFilePath, import { gracefulShutdown } from '../../utils/gracefulShutdown.js';
ensureConfigScope, import { safeParseJSON } from '../../utils/json.js';
getScopeLabel, import { getPlatform } from '../../utils/platform.js';
} from '../../services/mcp/utils.js' import { cliError, cliOk } from '../exit.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( async function checkMcpServerHealth(name: string, server: ScopedMcpServerConfig): Promise<string> {
name: string,
server: ScopedMcpServerConfig,
): Promise<string> {
try { try {
const result = await connectToServer(name, server) const result = await connectToServer(name, server);
if (result.type === 'connected') { if (result.type === 'connected') {
return '✓ Connected' return '✓ Connected';
} else if (result.type === 'needs-auth') { } else if (result.type === 'needs-auth') {
return '! Needs authentication' return '! Needs authentication';
} else { } else {
return '✗ Failed to connect' return '✗ Failed to connect';
} }
} catch (_error) { } catch (_error) {
return '✗ Connection error' return '✗ Connection error';
} }
} }
// mcp serve (lines 45124532) // mcp serve (lines 45124532)
export async function mcpServeHandler({ export async function mcpServeHandler({ debug, verbose }: { debug?: boolean; verbose?: boolean }): Promise<void> {
debug, const providedCwd = cwd();
verbose, logEvent('tengu_mcp_start', {});
}: {
debug?: boolean
verbose?: boolean
}): Promise<void> {
const providedCwd = cwd()
logEvent('tengu_mcp_start', {})
try { try {
await stat(providedCwd) await stat(providedCwd);
} catch (error) { } catch (error) {
if (isFsInaccessible(error)) { if (isFsInaccessible(error)) {
cliError(`Error: Directory ${providedCwd} does not exist`) cliError(`Error: Directory ${providedCwd} does not exist`);
} }
throw error throw error;
} }
try { try {
const { setup } = await import('../../setup.js') const { setup } = await import('../../setup.js');
await setup(providedCwd, 'default', false, false, undefined, false) await setup(providedCwd, 'default', false, false, undefined, false);
const { startMCPServer } = await import('../../entrypoints/mcp.js') const { startMCPServer } = await import('../../entrypoints/mcp.js');
await startMCPServer(providedCwd, debug ?? false, verbose ?? false) await startMCPServer(providedCwd, debug ?? false, verbose ?? false);
} catch (error) { } catch (error) {
cliError(`Error: Failed to start MCP server: ${error}`) cliError(`Error: Failed to start MCP server: ${error}`);
} }
} }
// mcp remove (lines 45454635) // mcp remove (lines 45454635)
export async function mcpRemoveHandler( export async function mcpRemoveHandler(name: string, options: { scope?: string }): Promise<void> {
name: string,
options: { scope?: string },
): Promise<void> {
// Look up config before removing so we can clean up secure storage // Look up config before removing so we can clean up secure storage
const serverBeforeRemoval = getMcpConfigByName(name) const serverBeforeRemoval = getMcpConfigByName(name);
const cleanupSecureStorage = () => { const cleanupSecureStorage = () => {
if ( if (serverBeforeRemoval && (serverBeforeRemoval.type === 'sse' || serverBeforeRemoval.type === 'http')) {
serverBeforeRemoval && clearServerTokensFromLocalStorage(name, serverBeforeRemoval);
(serverBeforeRemoval.type === 'sse' || clearMcpClientConfig(name, serverBeforeRemoval);
serverBeforeRemoval.type === 'http')
) {
clearServerTokensFromLocalStorage(name, serverBeforeRemoval)
clearMcpClientConfig(name, serverBeforeRemoval)
} }
} };
try { try {
if (options.scope) { if (options.scope) {
const scope = ensureConfigScope(options.scope) const scope = ensureConfigScope(options.scope);
logEvent('tengu_mcp_delete', { logEvent('tengu_mcp_delete', {
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope: scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, });
})
await removeMcpConfig(name, scope) await removeMcpConfig(name, scope);
cleanupSecureStorage() cleanupSecureStorage();
process.stdout.write(`Removed MCP server ${name} from ${scope} config\n`) process.stdout.write(`Removed MCP server ${name} from ${scope} config\n`);
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`) cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`);
} }
// If no scope specified, check where the server exists // If no scope specified, check where the server exists
const projectConfig = getCurrentProjectConfig() const projectConfig = getCurrentProjectConfig();
const globalConfig = getGlobalConfig() const globalConfig = getGlobalConfig();
// Check if server exists in project scope (.mcp.json) // Check if server exists in project scope (.mcp.json)
const { servers: projectServers } = getMcpConfigsByScope('project') const { servers: projectServers } = getMcpConfigsByScope('project');
const mcpJsonExists = !!projectServers[name] const mcpJsonExists = !!projectServers[name];
// Count how many scopes contain this server // Count how many scopes contain this server
const scopes: Array<Exclude<ConfigScope, 'dynamic'>> = [] const scopes: Array<Exclude<ConfigScope, 'dynamic'>> = [];
if (projectConfig.mcpServers?.[name]) scopes.push('local') if (projectConfig.mcpServers?.[name]) scopes.push('local');
if (mcpJsonExists) scopes.push('project') if (mcpJsonExists) scopes.push('project');
if (globalConfig.mcpServers?.[name]) scopes.push('user') if (globalConfig.mcpServers?.[name]) scopes.push('user');
if (scopes.length === 0) { 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) { } else if (scopes.length === 1) {
// Server exists in only one scope, remove it // Server exists in only one scope, remove it
const scope = scopes[0]! const scope = scopes[0]!;
logEvent('tengu_mcp_delete', { logEvent('tengu_mcp_delete', {
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope: scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, });
})
await removeMcpConfig(name, scope) await removeMcpConfig(name, scope);
cleanupSecureStorage() cleanupSecureStorage();
process.stdout.write( process.stdout.write(`Removed MCP server "${name}" from ${scope} config\n`);
`Removed MCP server "${name}" from ${scope} config\n`, cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`);
)
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`)
} else { } else {
// Server exists in multiple scopes // 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 => { scopes.forEach(scope => {
process.stderr.write( process.stderr.write(` - ${getScopeLabel(scope)} (${describeMcpConfigFilePath(scope)})\n`);
` - ${getScopeLabel(scope)} (${describeMcpConfigFilePath(scope)})\n`, });
) process.stderr.write('\nTo remove from a specific scope, use:\n');
})
process.stderr.write('\nTo remove from a specific scope, use:\n')
scopes.forEach(scope => { scopes.forEach(scope => {
process.stderr.write(` claude mcp remove "${name}" -s ${scope}\n`) process.stderr.write(` claude mcp remove "${name}" -s ${scope}\n`);
}) });
cliError() cliError();
} }
} catch (error) { } catch (error) {
cliError((error as Error).message) cliError((error as Error).message);
} }
} }
// mcp list (lines 46414688) // mcp list (lines 46414688)
export async function mcpListHandler(): Promise<void> { export async function mcpListHandler(): Promise<void> {
logEvent('tengu_mcp_list', {}) logEvent('tengu_mcp_list', {});
const { servers: configs } = await getAllMcpConfigs() const { servers: configs } = await getAllMcpConfigs();
if (Object.keys(configs).length === 0) { 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 { } 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 // Check servers concurrently
const entries = Object.entries(configs) const entries = Object.entries(configs);
const results = await pMap( const results = await pMap(
entries, entries,
async ([name, server]) => ({ async ([name, server]) => ({
@@ -208,127 +168,100 @@ export async function mcpListHandler(): Promise<void> {
status: await checkMcpServerHealth(name, server), status: await checkMcpServerHealth(name, server),
}), }),
{ concurrency: getMcpServerConnectionBatchSize() }, { concurrency: getMcpServerConnectionBatchSize() },
) );
for (const { name, server, status } of results) { for (const { name, server, status } of results) {
// Intentionally excluding sse-ide servers here since they're internal // Intentionally excluding sse-ide servers here since they're internal
if (server.type === 'sse') { 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') { } 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') { } 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') { } else if (!server.type || server.type === 'stdio') {
const stdioServer = server as { command: string; args: string[]; type?: string } const stdioServer = server as { command: string; args: string[]; type?: string };
const args = Array.isArray(stdioServer.args) ? stdioServer.args : [] const args = Array.isArray(stdioServer.args) ? stdioServer.args : [];
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log(`${name}: ${stdioServer.command} ${args.join(' ')} - ${status}`);
console.log(`${name}: ${stdioServer.command} ${args.join(' ')} - ${status}`)
} }
} }
} }
// Use gracefulShutdown to properly clean up MCP server connections // Use gracefulShutdown to properly clean up MCP server connections
// (process.exit bypasses cleanup handlers, leaving child processes orphaned) // (process.exit bypasses cleanup handlers, leaving child processes orphaned)
await gracefulShutdown(0) await gracefulShutdown(0);
} }
// mcp get (lines 46944786) // mcp get (lines 46944786)
export async function mcpGetHandler(name: string): Promise<void> { export async function mcpGetHandler(name: string): Promise<void> {
logEvent('tengu_mcp_get', { logEvent('tengu_mcp_get', {
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
}) });
const server = getMcpConfigByName(name) const server = getMcpConfigByName(name);
if (!server) { 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}:`);
console.log(`${name}:`) console.log(` Scope: ${getScopeLabel(server.scope)}`);
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Scope: ${getScopeLabel(server.scope)}`)
// Check server health // Check server health
const status = await checkMcpServerHealth(name, server) const status = await checkMcpServerHealth(name, server);
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Status: ${status}`);
console.log(` Status: ${status}`)
// Intentionally excluding sse-ide servers here since they're internal // Intentionally excluding sse-ide servers here since they're internal
if (server.type === 'sse') { if (server.type === 'sse') {
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Type: sse`);
console.log(` Type: sse`) console.log(` URL: ${server.url}`);
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` URL: ${server.url}`)
if (server.headers) { 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)) { 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) { if (server.oauth?.clientId || server.oauth?.callbackPort) {
const parts: string[] = [] const parts: string[] = [];
if (server.oauth.clientId) { if (server.oauth.clientId) {
parts.push('client_id configured') parts.push('client_id configured');
const clientConfig = getMcpClientConfig(name, server) const clientConfig = getMcpClientConfig(name, server);
if (clientConfig?.clientSecret) parts.push('client_secret configured') if (clientConfig?.clientSecret) parts.push('client_secret configured');
} }
if (server.oauth.callbackPort) if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
parts.push(`callback_port ${server.oauth.callbackPort}`) console.log(` OAuth: ${parts.join(', ')}`);
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` OAuth: ${parts.join(', ')}`)
} }
} else if (server.type === 'http') { } else if (server.type === 'http') {
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Type: http`);
console.log(` Type: http`) console.log(` URL: ${server.url}`);
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` URL: ${server.url}`)
if (server.headers) { 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)) { 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) { if (server.oauth?.clientId || server.oauth?.callbackPort) {
const parts: string[] = [] const parts: string[] = [];
if (server.oauth.clientId) { if (server.oauth.clientId) {
parts.push('client_id configured') parts.push('client_id configured');
const clientConfig = getMcpClientConfig(name, server) const clientConfig = getMcpClientConfig(name, server);
if (clientConfig?.clientSecret) parts.push('client_secret configured') if (clientConfig?.clientSecret) parts.push('client_secret configured');
} }
if (server.oauth.callbackPort) if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
parts.push(`callback_port ${server.oauth.callbackPort}`) console.log(` OAuth: ${parts.join(', ')}`);
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` OAuth: ${parts.join(', ')}`)
} }
} else if (server.type === 'stdio') { } else if (server.type === 'stdio') {
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Type: stdio`);
console.log(` Type: stdio`) console.log(` Command: ${server.command}`);
// biome-ignore lint/suspicious/noConsole:: intentional console output const args = Array.isArray(server.args) ? server.args : [];
console.log(` Command: ${server.command}`) console.log(` Args: ${args.join(' ')}`);
const args = Array.isArray(server.args) ? server.args : []
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Args: ${args.join(' ')}`)
if (server.env) { 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)) { 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 // Use gracefulShutdown to properly clean up MCP server connections
// (process.exit bypasses cleanup handlers, leaving child processes orphaned) // (process.exit bypasses cleanup handlers, leaving child processes orphaned)
await gracefulShutdown(0) await gracefulShutdown(0);
} }
// mcp add-json (lines 48014870) // mcp add-json (lines 48014870)
@@ -338,8 +271,8 @@ export async function mcpAddJsonHandler(
options: { scope?: string; clientSecret?: true }, options: { scope?: string; clientSecret?: true },
): Promise<void> { ): Promise<void> {
try { try {
const scope = ensureConfigScope(options.scope) const scope = ensureConfigScope(options.scope);
const parsedJson = safeParseJSON(json) const parsedJson = safeParseJSON(json);
// Read secret before writing config so cancellation doesn't leave partial state // Read secret before writing config so cancellation doesn't leave partial state
const needsSecret = const needsSecret =
@@ -353,15 +286,15 @@ export async function mcpAddJsonHandler(
'oauth' in parsedJson && 'oauth' in parsedJson &&
parsedJson.oauth && parsedJson.oauth &&
typeof parsedJson.oauth === 'object' && typeof parsedJson.oauth === 'object' &&
'clientId' in parsedJson.oauth 'clientId' in parsedJson.oauth;
const clientSecret = needsSecret ? await readClientSecret() : undefined const clientSecret = needsSecret ? await readClientSecret() : undefined;
await addMcpConfig(name, parsedJson, scope) await addMcpConfig(name, parsedJson, scope);
const transportType = const transportType =
parsedJson && typeof parsedJson === 'object' && 'type' in parsedJson parsedJson && typeof parsedJson === 'object' && 'type' in parsedJson
? String(parsedJson.type || 'stdio') ? String(parsedJson.type || 'stdio')
: 'stdio' : 'stdio';
if ( if (
clientSecret && clientSecret &&
@@ -372,53 +305,38 @@ export async function mcpAddJsonHandler(
'url' in parsedJson && 'url' in parsedJson &&
typeof parsedJson.url === 'string' typeof parsedJson.url === 'string'
) { ) {
saveMcpClientSecret( saveMcpClientSecret(name, { type: parsedJson.type, url: parsedJson.url }, clientSecret);
name,
{ type: parsedJson.type, url: parsedJson.url },
clientSecret,
)
} }
logEvent('tengu_mcp_add', { logEvent('tengu_mcp_add', {
scope: scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, source: 'json' 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, 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) { } catch (error) {
cliError((error as Error).message) cliError((error as Error).message);
} }
} }
// mcp add-from-claude-desktop (lines 48814927) // mcp add-from-claude-desktop (lines 48814927)
export async function mcpAddFromDesktopHandler(options: { export async function mcpAddFromDesktopHandler(options: { scope?: string }): Promise<void> {
scope?: string
}): Promise<void> {
try { try {
const scope = ensureConfigScope(options.scope) const scope = ensureConfigScope(options.scope);
const platform = getPlatform() const platform = getPlatform();
logEvent('tengu_mcp_add', { logEvent('tengu_mcp_add', {
scope: scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, platform: platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
platform: source: 'desktop' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
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( const { readClaudeDesktopMcpServers } = await import('../../utils/claudeDesktop.js');
'../../utils/claudeDesktop.js' const servers = await readClaudeDesktopMcpServers();
)
const servers = await readClaudeDesktopMcpServers()
if (Object.keys(servers).length === 0) { if (Object.keys(servers).length === 0) {
cliOk( cliOk('No MCP servers found in Claude Desktop configuration or configuration file does not exist.');
'No MCP servers found in Claude Desktop configuration or configuration file does not exist.',
)
} }
const { unmount } = await render( const { unmount } = await render(
@@ -428,29 +346,29 @@ export async function mcpAddFromDesktopHandler(options: {
servers={servers} servers={servers}
scope={scope} scope={scope}
onDone={() => { onDone={() => {
unmount() unmount();
}} }}
/> />
</KeybindingSetup> </KeybindingSetup>
</AppStateProvider>, </AppStateProvider>,
{ exitOnCtrlC: true }, { exitOnCtrlC: true },
) );
} catch (error) { } catch (error) {
cliError((error as Error).message) cliError((error as Error).message);
} }
} }
// mcp reset-project-choices (lines 49354952) // mcp reset-project-choices (lines 49354952)
export async function mcpResetChoicesHandler(): Promise<void> { export async function mcpResetChoicesHandler(): Promise<void> {
logEvent('tengu_mcp_reset_mcpjson_choices', {}) logEvent('tengu_mcp_reset_mcpjson_choices', {});
saveCurrentProjectConfig(current => ({ saveCurrentProjectConfig(current => ({
...current, ...current,
enabledMcpjsonServers: [], enabledMcpjsonServers: [],
disabledMcpjsonServers: [], disabledMcpjsonServers: [],
enableAllProjectMcpServers: false, enableAllProjectMcpServers: false,
})) }));
cliOk( cliOk(
'All project-scoped (.mcp.json) server approvals and rejections have been reset.\n' + '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.', '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 { function printValidationResult(result: ValidationResult): void {
if (result.errors.length > 0) { if (result.errors.length > 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log( console.log(
`${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n`, `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n`,
) )
result.errors.forEach(error => { result.errors.forEach(error => {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${error.path}: ${error.message}`) console.log(` ${figures.pointer} ${error.path}: ${error.message}`)
}) })
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('') console.log('')
} }
if (result.warnings.length > 0) { if (result.warnings.length > 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log( console.log(
`${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n`, `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n`,
) )
result.warnings.forEach(warning => { result.warnings.forEach(warning => {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${warning.path}: ${warning.message}`) console.log(` ${figures.pointer} ${warning.path}: ${warning.message}`)
}) })
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('') console.log('')
} }
} }
@@ -106,7 +100,6 @@ export async function pluginValidateHandler(
try { try {
const result = await validateManifest(manifestPath) const result = await validateManifest(manifestPath)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Validating ${result.fileType} manifest: ${result.filePath}\n`) console.log(`Validating ${result.fileType} manifest: ${result.filePath}\n`)
printValidationResult(result) printValidationResult(result)
@@ -120,7 +113,6 @@ export async function pluginValidateHandler(
if (basename(manifestDir) === '.claude-plugin') { if (basename(manifestDir) === '.claude-plugin') {
contentResults = await validatePluginContents(dirname(manifestDir)) contentResults = await validatePluginContents(dirname(manifestDir))
for (const r of contentResults) { for (const r of contentResults) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Validating ${r.fileType}: ${r.filePath}\n`) console.log(`Validating ${r.fileType}: ${r.filePath}\n`)
printValidationResult(r) printValidationResult(r)
} }
@@ -139,13 +131,11 @@ export async function pluginValidateHandler(
: `${figures.tick} Validation passed`, : `${figures.tick} Validation passed`,
) )
} else { } else {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`${figures.cross} Validation failed`) console.log(`${figures.cross} Validation failed`)
process.exit(1) process.exit(1)
} }
} catch (error) { } catch (error) {
logError(error) logError(error)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error( console.error(
`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`, `${figures.cross} Unexpected error during validation: ${errorMessage(error)}`,
) )
@@ -358,7 +348,6 @@ export async function pluginListHandler(options: {
} }
if (pluginIds.length > 0) { if (pluginIds.length > 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('Installed plugins:\n') console.log('Installed plugins:\n')
} }
@@ -383,25 +372,18 @@ export async function pluginListHandler(options: {
const version = installation.version || 'unknown' const version = installation.version || 'unknown'
const scope = installation.scope const scope = installation.scope
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${pluginId}`) console.log(` ${figures.pointer} ${pluginId}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Version: ${version}`) console.log(` Version: ${version}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Scope: ${scope}`) console.log(` Scope: ${scope}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Status: ${status}`) console.log(` Status: ${status}`)
for (const error of pluginErrors) { for (const error of pluginErrors) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Error: ${getPluginErrorMessage(error)}`) console.log(` Error: ${getPluginErrorMessage(error)}`)
} }
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('') console.log('')
} }
} }
if (inlinePlugins.length > 0 || inlineLoadErrors.length > 0) { if (inlinePlugins.length > 0 || inlineLoadErrors.length > 0) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('Session-only plugins (--plugin-dir):\n') console.log('Session-only plugins (--plugin-dir):\n')
for (const p of inlinePlugins) { for (const p of inlinePlugins) {
// Same dirName≠manifestName fallback as the JSON path above — error // Same dirName≠manifestName fallback as the JSON path above — error
@@ -413,19 +395,13 @@ export async function pluginListHandler(options: {
pErrors.length > 0 pErrors.length > 0
? `${figures.cross} loaded with errors` ? `${figures.cross} loaded with errors`
: `${figures.tick} loaded` : `${figures.tick} loaded`
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${p.source}`) console.log(` ${figures.pointer} ${p.source}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Version: ${p.manifest.version ?? 'unknown'}`) console.log(` Version: ${p.manifest.version ?? 'unknown'}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Path: ${p.path}`) console.log(` Path: ${p.path}`)
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Status: ${status}`) console.log(` Status: ${status}`)
for (const e of pErrors) { for (const e of pErrors) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Error: ${getPluginErrorMessage(e)}`) console.log(` Error: ${getPluginErrorMessage(e)}`)
} }
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('') console.log('')
} }
// Path-level failures: no LoadedPlugin object exists. Show them so // 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 => for (const e of inlineLoadErrors.filter(e =>
e.source.startsWith('inline['), e.source.startsWith('inline['),
)) { )) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log( console.log(
` ${figures.pointer} ${e.source}: ${figures.cross} ${getPluginErrorMessage(e)}\n`, ` ${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...') console.log('Adding marketplace...')
const { name, alreadyMaterialized, resolvedSource } = const { name, alreadyMaterialized, resolvedSource } =
await addMarketplaceSource(marketplaceSource, message => { await addMarketplaceSource(marketplaceSource, message => {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(message) console.log(message)
}) })
@@ -555,33 +528,25 @@ export async function marketplaceListHandler(options: {
cliOk('No marketplaces configured') cliOk('No marketplaces configured')
} }
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('Configured marketplaces:\n') console.log('Configured marketplaces:\n')
names.forEach(name => { names.forEach(name => {
const marketplace = config[name] const marketplace = config[name]
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` ${figures.pointer} ${name}`) console.log(` ${figures.pointer} ${name}`)
if (marketplace?.source) { if (marketplace?.source) {
const src = marketplace.source const src = marketplace.source
if (src.source === 'github') { if (src.source === 'github') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: GitHub (${src.repo})`) console.log(` Source: GitHub (${src.repo})`)
} else if (src.source === 'git') { } else if (src.source === 'git') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: Git (${src.url})`) console.log(` Source: Git (${src.url})`)
} else if (src.source === 'url') { } else if (src.source === 'url') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: URL (${src.url})`) console.log(` Source: URL (${src.url})`)
} else if (src.source === 'directory') { } else if (src.source === 'directory') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: Directory (${src.path})`) console.log(` Source: Directory (${src.path})`)
} else if (src.source === 'file') { } else if (src.source === 'file') {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(` Source: File (${src.path})`) console.log(` Source: File (${src.path})`)
} }
} }
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log('') console.log('')
}) })
@@ -620,11 +585,9 @@ export async function marketplaceUpdateHandler(
if (options.cowork) setUseCoworkPlugins(true) if (options.cowork) setUseCoworkPlugins(true)
try { try {
if (name) { if (name) {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Updating marketplace: ${name}...`) console.log(`Updating marketplace: ${name}...`)
await refreshMarketplace(name, message => { await refreshMarketplace(name, message => {
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(message) console.log(message)
}) })
@@ -644,7 +607,6 @@ export async function marketplaceUpdateHandler(
cliOk('No marketplaces configured') cliOk('No marketplaces configured')
} }
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.log(`Updating ${marketplaceNames.length} marketplace(s)...`) console.log(`Updating ${marketplaceNames.length} marketplace(s)...`)
await refreshAllMarketplaces() await refreshAllMarketplaces()

View File

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

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // 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 // 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 // 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 // Auto-generated type stub — replace with real implementation
export type SDKStatus = any; export type SDKStatus = any
export type ModelInfo = any; export type ModelInfo = any
export type SDKMessage = any; export type SDKMessage = any
export type SDKUserMessage = any; export type SDKUserMessage = any
export type SDKUserMessageReplay = any; export type SDKUserMessageReplay = any
export type PermissionResult = any; export type PermissionResult = any
export type McpServerConfigForProcessTransport = any; export type McpServerConfigForProcessTransport = any
export type McpServerStatus = any; export type McpServerStatus = any
export type RewindFilesResult = any; export type RewindFilesResult = any
export type HookEvent = any; export type HookEvent = any
export type HookInput = any; export type HookInput = any
export type HookJSONOutput = any; export type HookJSONOutput = any
export type PermissionUpdate = any; export type PermissionUpdate = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // Auto-generated type stub — replace with real implementation
export type StdoutMessage = any; export type StdoutMessage = any
export type SDKControlInitializeRequest = any; export type SDKControlInitializeRequest = any
export type SDKControlInitializeResponse = any; export type SDKControlInitializeResponse = any
export type SDKControlRequest = any; export type SDKControlRequest = any
export type SDKControlResponse = any; export type SDKControlResponse = any
export type SDKControlMcpSetServersResponse = any; export type SDKControlMcpSetServersResponse = any
export type SDKControlReloadPluginsResponse = any; export type SDKControlReloadPluginsResponse = any
export type StdinMessage = any; export type StdinMessage = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // Auto-generated type stub — replace with real implementation
export type tryGenerateSuggestion = any; export type tryGenerateSuggestion = any
export type logSuggestionOutcome = any; export type logSuggestionOutcome = any
export type logSuggestionSuppressed = any; export type logSuggestionSuppressed = any
export type PromptVariant = any; export type PromptVariant = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // Auto-generated type stub — replace with real implementation
export type logEvent = any; export type logEvent = any
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = 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 // Auto-generated type stub — replace with real implementation
export type isQualifiedForGrove = any; export type isQualifiedForGrove = any
export type checkGroveForNonInteractive = any; export type checkGroveForNonInteractive = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // Auto-generated type stub — replace with real implementation
export type statusListeners = any; export type statusListeners = any
export type ClaudeAILimits = any; export type ClaudeAILimits = any

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // Auto-generated type stub — replace with real implementation
export type MCPServerConnection = any; export type MCPServerConnection = any
export type McpSdkServerConfig = any; export type McpSdkServerConfig = any
export type ScopedMcpServerConfig = any; export type ScopedMcpServerConfig = any

View File

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

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // 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 // 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 // Auto-generated type stub — replace with real implementation
export type downloadUserSettings = any; export type downloadUserSettings = any
export type redownloadUserSettings = any; export type redownloadUserSettings = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // Auto-generated type stub — replace with real implementation
export type assembleToolPool = any; export type assembleToolPool = any
export type filterToolsByDenyRules = any; export type filterToolsByDenyRules = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // 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 // Auto-generated type stub — replace with real implementation
export type getLatestVersion = any; export type getLatestVersion = any
export type InstallStatus = any; export type InstallStatus = any
export type installGlobalPackage = any; export type installGlobalPackage = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // 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 // 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 // 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 // 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 // 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 // Auto-generated type stub — replace with real implementation
export type getGlobalConfig = any; export type getGlobalConfig = any
export type InstallMethod = any; export type InstallMethod = any
export type saveGlobalConfig = any; export type saveGlobalConfig = any

View File

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

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // Auto-generated type stub — replace with real implementation
export type logForDiagnosticsNoPII = any; export type logForDiagnosticsNoPII = any
export type withDiagnosticsTiming = any; export type withDiagnosticsTiming = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // Auto-generated type stub — replace with real implementation
export type modelSupportsEffort = any; export type modelSupportsEffort = any
export type modelSupportsMaxEffort = any; export type modelSupportsMaxEffort = any
export type EFFORT_LEVELS = any; export type EFFORT_LEVELS = any
export type resolveAppliedEffort = any; export type resolveAppliedEffort = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // Auto-generated type stub — replace with real implementation
export type isFastModeAvailable = any; export type isFastModeAvailable = any
export type isFastModeEnabled = any; export type isFastModeEnabled = any
export type isFastModeSupportedByModel = any; export type isFastModeSupportedByModel = any
export type getFastModeState = any; export type getFastModeState = any

View File

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

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // Auto-generated type stub — replace with real implementation
export type createFileStateCacheWithSizeLimit = any; export type createFileStateCacheWithSizeLimit = any
export type mergeFileStateCaches = any; export type mergeFileStateCaches = any
export type READ_FILE_STATE_CACHE_SIZE = any; export type READ_FILE_STATE_CACHE_SIZE = any

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // Auto-generated type stub — replace with real implementation
export type gracefulShutdown = any; export type gracefulShutdown = any
export type gracefulShutdownSync = any; export type gracefulShutdownSync = any
export type isShuttingDown = any; export type isShuttingDown = any

View File

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

View File

@@ -1,2 +1,2 @@
// Auto-generated type stub — replace with real implementation // 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 // 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 // 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 // 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 // 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