mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
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:
@@ -41,7 +41,11 @@ import { type Tools, type ToolUseContext, toolMatchesName } from './Tool.js'
|
||||
import type { AgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
|
||||
import { SYNTHETIC_OUTPUT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SyntheticOutputTool/SyntheticOutputTool.js'
|
||||
import type { APIError } from '@anthropic-ai/sdk'
|
||||
import type { CompactMetadata, Message, SystemCompactBoundaryMessage } from './types/message.js'
|
||||
import type {
|
||||
CompactMetadata,
|
||||
Message,
|
||||
SystemCompactBoundaryMessage,
|
||||
} from './types/message.js'
|
||||
import type { OrphanedPermission } from './types/textInputTypes.js'
|
||||
import { createAbortController } from './utils/abortController.js'
|
||||
import type { AttributionState } from './utils/commitAttribution.js'
|
||||
@@ -708,7 +712,8 @@ export class QueryEngine {
|
||||
message.subtype === 'compact_boundary'
|
||||
) {
|
||||
const compactMsg = message as SystemCompactBoundaryMessage
|
||||
const tailUuid = compactMsg.compactMetadata?.preservedSegment?.tailUuid
|
||||
const tailUuid =
|
||||
compactMsg.compactMetadata?.preservedSegment?.tailUuid
|
||||
if (tailUuid) {
|
||||
const tailIdx = this.mutableMessages.findLastIndex(
|
||||
m => m.uuid === tailUuid,
|
||||
@@ -768,7 +773,10 @@ export class QueryEngine {
|
||||
// streamed responses, this is null at content_block_stop time;
|
||||
// the real value arrives via message_delta (handled below).
|
||||
const msg = message as Message
|
||||
const stopReason = msg.message?.stop_reason as string | null | undefined
|
||||
const stopReason = msg.message?.stop_reason as
|
||||
| string
|
||||
| null
|
||||
| undefined
|
||||
if (stopReason != null) {
|
||||
lastStopReason = stopReason
|
||||
}
|
||||
@@ -798,11 +806,15 @@ export class QueryEngine {
|
||||
break
|
||||
}
|
||||
case 'stream_event': {
|
||||
const event = (message as unknown as { event: Record<string, unknown> }).event
|
||||
const event = (
|
||||
message as unknown as { event: Record<string, unknown> }
|
||||
).event
|
||||
if (event.type === 'message_start') {
|
||||
// Reset current message usage for new message
|
||||
currentMessageUsage = EMPTY_USAGE
|
||||
const eventMessage = event.message as { usage: BetaMessageDeltaUsage }
|
||||
const eventMessage = event.message as {
|
||||
usage: BetaMessageDeltaUsage
|
||||
}
|
||||
currentMessageUsage = updateUsage(
|
||||
currentMessageUsage,
|
||||
eventMessage.usage,
|
||||
@@ -851,7 +863,15 @@ export class QueryEngine {
|
||||
void recordTranscript(messages)
|
||||
}
|
||||
|
||||
const attachment = msg.attachment as { type: string; data?: unknown; turnCount?: number; maxTurns?: number; prompt?: string; source_uuid?: string; [key: string]: unknown }
|
||||
const attachment = msg.attachment as {
|
||||
type: string
|
||||
data?: unknown
|
||||
turnCount?: number
|
||||
maxTurns?: number
|
||||
prompt?: string
|
||||
source_uuid?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// Extract structured output from StructuredOutput tool calls
|
||||
if (attachment.type === 'structured_output') {
|
||||
@@ -892,10 +912,7 @@ export class QueryEngine {
|
||||
return
|
||||
}
|
||||
// Yield queued_command attachments as SDK user message replays
|
||||
else if (
|
||||
replayUserMessages &&
|
||||
attachment.type === 'queued_command'
|
||||
) {
|
||||
else if (replayUserMessages && attachment.type === 'queued_command') {
|
||||
yield {
|
||||
type: 'user',
|
||||
message: {
|
||||
@@ -923,10 +940,7 @@ export class QueryEngine {
|
||||
// never shrinks (memory leak in long SDK sessions). The subtype
|
||||
// check lives inside the injected callback so feature-gated strings
|
||||
// stay out of this file (excluded-strings check).
|
||||
const snipResult = this.config.snipReplay?.(
|
||||
msg,
|
||||
this.mutableMessages,
|
||||
)
|
||||
const snipResult = this.config.snipReplay?.(msg, this.mutableMessages)
|
||||
if (snipResult !== undefined) {
|
||||
if (snipResult.executed) {
|
||||
this.mutableMessages.length = 0
|
||||
@@ -936,10 +950,7 @@ export class QueryEngine {
|
||||
}
|
||||
this.mutableMessages.push(msg)
|
||||
// Yield compact boundary messages to SDK
|
||||
if (
|
||||
msg.subtype === 'compact_boundary' &&
|
||||
msg.compactMetadata
|
||||
) {
|
||||
if (msg.subtype === 'compact_boundary' && msg.compactMetadata) {
|
||||
const compactMsg = msg as SystemCompactBoundaryMessage
|
||||
// Release pre-compaction messages for GC. The boundary was just
|
||||
// pushed so it's the last element. query.ts already uses
|
||||
@@ -959,11 +970,18 @@ export class QueryEngine {
|
||||
subtype: 'compact_boundary' as const,
|
||||
session_id: getSessionId(),
|
||||
uuid: msg.uuid,
|
||||
compact_metadata: toSDKCompactMetadata(compactMsg.compactMetadata),
|
||||
compact_metadata: toSDKCompactMetadata(
|
||||
compactMsg.compactMetadata,
|
||||
),
|
||||
}
|
||||
}
|
||||
if (msg.subtype === 'api_error') {
|
||||
const apiErrorMsg = msg as Message & { retryAttempt: number; maxRetries: number; retryInMs: number; error: APIError }
|
||||
const apiErrorMsg = msg as Message & {
|
||||
retryAttempt: number
|
||||
maxRetries: number
|
||||
retryInMs: number
|
||||
error: APIError
|
||||
}
|
||||
yield {
|
||||
type: 'system',
|
||||
subtype: 'api_retry' as const,
|
||||
@@ -980,7 +998,10 @@ export class QueryEngine {
|
||||
break
|
||||
}
|
||||
case 'tool_use_summary': {
|
||||
const msg = message as Message & { summary: unknown; precedingToolUseIds: unknown }
|
||||
const msg = message as Message & {
|
||||
summary: unknown
|
||||
precedingToolUseIds: unknown
|
||||
}
|
||||
// Yield tool use summary messages to SDK
|
||||
yield {
|
||||
type: 'tool_use_summary' as const,
|
||||
@@ -1089,7 +1110,10 @@ export class QueryEngine {
|
||||
const edeResultType = result?.type ?? 'undefined'
|
||||
const edeLastContentType =
|
||||
result?.type === 'assistant'
|
||||
? (last(result.message!.content as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlock[])?.type ?? 'none')
|
||||
? (last(
|
||||
result.message!
|
||||
.content as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlock[],
|
||||
)?.type ?? 'none')
|
||||
: 'n/a'
|
||||
|
||||
// Flush buffered transcript writes before yielding result.
|
||||
@@ -1147,7 +1171,10 @@ export class QueryEngine {
|
||||
let isApiError = false
|
||||
|
||||
if (result.type === 'assistant') {
|
||||
const lastContent = last(result.message!.content as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlock[])
|
||||
const lastContent = last(
|
||||
result.message!
|
||||
.content as import('@anthropic-ai/sdk/resources/beta/messages/messages.js').BetaContentBlock[],
|
||||
)
|
||||
if (
|
||||
lastContent?.type === 'text' &&
|
||||
!SYNTHETIC_MESSAGES.has(lastContent.text)
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
setSystemPromptInjection,
|
||||
} from '../context'
|
||||
import { clearMemoryFileCaches } from '../utils/claudemd'
|
||||
import { cleanupTempDir, createTempDir, writeTempFile } from '../../tests/mocks/file-system'
|
||||
import {
|
||||
cleanupTempDir,
|
||||
createTempDir,
|
||||
writeTempFile,
|
||||
} from '../../tests/mocks/file-system'
|
||||
|
||||
let tempDir = ''
|
||||
let projectClaudeMdContent = ''
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type HookEvent = any;
|
||||
export type ModelUsage = any;
|
||||
export type HookEvent = any
|
||||
export type ModelUsage = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type AgentColorName = any;
|
||||
export type AgentColorName = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type HookCallbackMatcher = any;
|
||||
export type HookCallbackMatcher = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type SessionId = any;
|
||||
export type SessionId = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type randomUUID = any;
|
||||
export type randomUUID = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type ModelSetting = any;
|
||||
export type ModelSetting = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type ModelStrings = any;
|
||||
export type ModelStrings = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type SettingSource = any;
|
||||
export type SettingSource = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type resetSettingsCache = any;
|
||||
export type resetSettingsCache = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type PluginHookMatcher = any;
|
||||
export type PluginHookMatcher = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type createSignal = any;
|
||||
export type createSignal = any
|
||||
|
||||
@@ -1755,4 +1755,6 @@ export function getPromptId(): string | null {
|
||||
export function setPromptId(id: string | null): void {
|
||||
STATE.promptId = id
|
||||
}
|
||||
export function isReplBridgeActive(): boolean { return false; }
|
||||
export function isReplBridgeActive(): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -225,7 +225,9 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient {
|
||||
)
|
||||
|
||||
handleErrorStatus(response.status, response.data, 'Poll')
|
||||
rcLog(`poll response: status=${response.status} hasData=${!!response.data} url=${deps.baseUrl}`)
|
||||
rcLog(
|
||||
`poll response: status=${response.status} hasData=${!!response.data} url=${deps.baseUrl}`,
|
||||
)
|
||||
|
||||
// Empty body or null = no work available
|
||||
if (!response.data) {
|
||||
|
||||
@@ -448,9 +448,11 @@ export async function runBridgeLoop(
|
||||
): (status: SessionDoneStatus) => void {
|
||||
return (rawStatus: SessionDoneStatus): void => {
|
||||
const workId = sessionWorkIds.get(sessionId)
|
||||
rcLog(`session done: sessionId=${sessionId} workId=${workId ?? 'none'} status=${rawStatus}` +
|
||||
` wasTimedOut=${timedOutSessions.has(sessionId)} duration=${Math.round((Date.now() - startTime) / 1000)}s` +
|
||||
` stderr=${handle.lastStderr.length > 0 ? handle.lastStderr.join('\\n').slice(0, 500) : '(none)'}`)
|
||||
rcLog(
|
||||
`session done: sessionId=${sessionId} workId=${workId ?? 'none'} status=${rawStatus}` +
|
||||
` wasTimedOut=${timedOutSessions.has(sessionId)} duration=${Math.round((Date.now() - startTime) / 1000)}s` +
|
||||
` stderr=${handle.lastStderr.length > 0 ? handle.lastStderr.join('\\n').slice(0, 500) : '(none)'}`,
|
||||
)
|
||||
activeSessions.delete(sessionId)
|
||||
sessionStartTimes.delete(sessionId)
|
||||
sessionWorkIds.delete(sessionId)
|
||||
@@ -609,7 +611,9 @@ export async function runBridgeLoop(
|
||||
const pollConfig = getPollIntervalConfig()
|
||||
|
||||
try {
|
||||
rcLog(`poll: envId=${environmentId} activeSessions=${activeSessions.size}`)
|
||||
rcLog(
|
||||
`poll: envId=${environmentId} activeSessions=${activeSessions.size}`,
|
||||
)
|
||||
const work = await api.pollForWork(
|
||||
environmentId,
|
||||
environmentSecret,
|
||||
@@ -864,7 +868,9 @@ export async function runBridgeLoop(
|
||||
break
|
||||
case 'session': {
|
||||
const sessionId = work.data.id
|
||||
rcLog(`work received: type=session sessionId=${sessionId} workId=${work.id}`)
|
||||
rcLog(
|
||||
`work received: type=session sessionId=${sessionId} workId=${work.id}`,
|
||||
)
|
||||
try {
|
||||
validateBridgeId(sessionId, 'session_id')
|
||||
} catch {
|
||||
@@ -1032,9 +1038,9 @@ export async function runBridgeLoop(
|
||||
|
||||
rcLog(
|
||||
`spawning session: sessionId=${sessionId} sdkUrl=${sdkUrl}` +
|
||||
` useCcrV2=${useCcrV2} workerEpoch=${workerEpoch}` +
|
||||
` dir=${sessionDir}` +
|
||||
` accessToken=${secret.session_ingress_token ? secret.session_ingress_token.slice(0, 8) + '...' : 'NONE'}`,
|
||||
` useCcrV2=${useCcrV2} workerEpoch=${workerEpoch}` +
|
||||
` dir=${sessionDir}` +
|
||||
` accessToken=${secret.session_ingress_token ? secret.session_ingress_token.slice(0, 8) + '...' : 'NONE'}`,
|
||||
)
|
||||
const spawnResult = safeSpawn(
|
||||
spawner,
|
||||
@@ -1281,8 +1287,8 @@ export async function runBridgeLoop(
|
||||
const errMsg = describeAxiosError(err)
|
||||
rcLog(
|
||||
`poll error: ${errMsg}` +
|
||||
` isConn=${isConnectionError(err)} isServer=${isServerError(err)}` +
|
||||
` activeSessions=${activeSessions.size}`,
|
||||
` isConn=${isConnectionError(err)} isServer=${isServerError(err)}` +
|
||||
` activeSessions=${activeSessions.size}`,
|
||||
)
|
||||
|
||||
if (isConnectionError(err) || isServerError(err)) {
|
||||
@@ -1676,7 +1682,7 @@ async function stopWorkWithRetry(
|
||||
}
|
||||
const errMsg = errorMessage(err)
|
||||
if (attempt < MAX_ATTEMPTS) {
|
||||
const delay = addJitter(baseDelayMs * Math.pow(2, attempt - 1))
|
||||
const delay = addJitter(baseDelayMs * 2 ** (attempt - 1))
|
||||
logger.logVerbose(
|
||||
`Failed to stop work ${workId} (attempt ${attempt}/${MAX_ATTEMPTS}), retrying in ${formatDelay(delay)}: ${errMsg}`,
|
||||
)
|
||||
@@ -1964,7 +1970,6 @@ NOTES
|
||||
- You must be logged in with a Claude account that has a subscription
|
||||
- Run \`claude\` first in the directory to accept the workspace trust dialog
|
||||
${serverNote}`
|
||||
// biome-ignore lint/suspicious/noConsole: intentional help output
|
||||
console.log(help)
|
||||
}
|
||||
|
||||
@@ -2003,7 +2008,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
return
|
||||
}
|
||||
if (parsed.error) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(`Error: ${parsed.error}`)
|
||||
// eslint-disable-next-line custom-rules/no-process-exit
|
||||
process.exit(1)
|
||||
@@ -2042,7 +2046,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const { PERMISSION_MODES } = await import('../types/permissions.js')
|
||||
const valid: readonly string[] = PERMISSION_MODES
|
||||
if (!valid.includes(permissionMode)) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Invalid permission mode '${permissionMode}'. Valid modes: ${valid.join(', ')}`,
|
||||
)
|
||||
@@ -2085,7 +2088,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
Promise.all([shutdown1PEventLogging(), shutdownDatadog()]),
|
||||
sleep(500, undefined, { unref: true }),
|
||||
]).catch(() => {})
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
'Error: Multi-session Remote Control is not enabled for your account yet.',
|
||||
)
|
||||
@@ -2102,7 +2104,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
// The bridge bypasses main.tsx (which renders the interactive TrustDialog via showSetupScreens),
|
||||
// so we must verify trust was previously established by a normal `claude` session.
|
||||
if (!checkHasTrustDialogAccepted()) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
`Error: Workspace not trusted. Please run \`claude\` in ${dir} first to review and accept the workspace trust dialog.`,
|
||||
)
|
||||
@@ -2119,7 +2120,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
|
||||
const bridgeToken = getBridgeAccessToken()
|
||||
if (!bridgeToken) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(BRIDGE_LOGIN_ERROR)
|
||||
// eslint-disable-next-line custom-rules/no-process-exit
|
||||
process.exit(1)
|
||||
@@ -2138,7 +2138,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
'\nRemote Control lets you access this CLI session from the web (claude.ai/code)\nor the Claude app, so you can pick up where you left off on any device.\n\nYou can disconnect remote access anytime by running /remote-control again.\n',
|
||||
)
|
||||
@@ -2170,7 +2169,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
)
|
||||
const found = await readBridgePointerAcrossWorktrees(dir)
|
||||
if (!found) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: No recent session found in this directory or its worktrees. Run \`claude remote-control\` to start a new one.`,
|
||||
)
|
||||
@@ -2181,7 +2179,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const ageMin = Math.round(pointer.ageMs / 60_000)
|
||||
const ageStr = ageMin < 60 ? `${ageMin}m` : `${Math.round(ageMin / 60)}h`
|
||||
const fromWt = pointerDir !== dir ? ` from worktree ${pointerDir}` : ''
|
||||
// biome-ignore lint/suspicious/noConsole: intentional info output
|
||||
console.error(
|
||||
`Resuming session ${pointer.sessionId} (${ageStr} ago)${fromWt}\u2026`,
|
||||
)
|
||||
@@ -2202,7 +2199,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
!baseUrl.includes('localhost') &&
|
||||
!baseUrl.includes('127.0.0.1')
|
||||
) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
'Error: Remote Control base URL uses HTTP. Only HTTPS or localhost HTTP is allowed.',
|
||||
)
|
||||
@@ -2238,7 +2234,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
? getCurrentProjectConfig().remoteControlSpawnMode
|
||||
: undefined
|
||||
if (savedSpawnMode === 'worktree' && !worktreeAvailable) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional warning output
|
||||
console.error(
|
||||
'Warning: Saved spawn mode is worktree but this directory is not a git repository. Falling back to same-dir.',
|
||||
)
|
||||
@@ -2265,7 +2260,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
// biome-ignore lint/suspicious/noConsole: intentional dialog output
|
||||
console.log(
|
||||
`\nClaude Remote Control is launching in spawn mode which lets you create new sessions in this project from Claude Code on Web or your Mobile app. Learn more here: https://code.claude.com/docs/en/remote-control\n\n` +
|
||||
`Spawn mode for this project:\n` +
|
||||
@@ -2344,7 +2338,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
// Only reachable via explicit --spawn=worktree (default is same-dir);
|
||||
// saved worktree pref was already guarded above.
|
||||
if (spawnMode === 'worktree' && !worktreeAvailable) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Worktree mode requires a git repository or WorktreeCreate hooks configured. Use --spawn=session for single-session mode.`,
|
||||
)
|
||||
@@ -2379,7 +2372,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
try {
|
||||
validateBridgeId(resumeSessionId, 'sessionId')
|
||||
} catch {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Invalid session ID "${resumeSessionId}". Session IDs must not contain unsafe characters.`,
|
||||
)
|
||||
@@ -2405,7 +2397,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const { clearBridgePointer } = await import('./bridgePointer.js')
|
||||
await clearBridgePointer(resumePointerDir)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Session ${resumeSessionId} not found. It may have been archived or expired, or your login may have lapsed (run \`claude /login\`).`,
|
||||
)
|
||||
@@ -2417,7 +2408,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const { clearBridgePointer } = await import('./bridgePointer.js')
|
||||
await clearBridgePointer(resumePointerDir)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Session ${resumeSessionId} has no environment_id. It may never have been attached to a bridge.`,
|
||||
)
|
||||
@@ -2471,7 +2461,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
status: err instanceof BridgeFatalError ? err.status : undefined,
|
||||
})
|
||||
// Registration failures are fatal — print a clean message instead of a stack trace.
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
err instanceof BridgeFatalError && err.status === 404
|
||||
? 'Remote Control environments are not available for your account.'
|
||||
@@ -2496,7 +2485,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
`Bridge resume env mismatch: requested ${reuseEnvironmentId}, backend returned ${environmentId}. Falling back to fresh session.`,
|
||||
),
|
||||
)
|
||||
// biome-ignore lint/suspicious/noConsole: intentional warning output
|
||||
console.warn(
|
||||
`Warning: Could not resume session ${resumeSessionId} — its environment has expired. Creating a fresh session instead.`,
|
||||
)
|
||||
@@ -2547,7 +2535,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const { clearBridgePointer } = await import('./bridgePointer.js')
|
||||
await clearBridgePointer(resumePointerDir)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
isFatal
|
||||
? `Error: ${errorMessage(err)}`
|
||||
|
||||
@@ -104,7 +104,8 @@ export function isEligibleBridgeMessage(m: Message): boolean {
|
||||
export function extractTitleText(m: Message): string | undefined {
|
||||
if (m.type !== 'user' || m.isMeta || m.toolUseResult || m.isCompactSummary)
|
||||
return undefined
|
||||
if (m.origin && (m.origin as { kind?: string }).kind !== 'human') return undefined
|
||||
if (m.origin && (m.origin as { kind?: string }).kind !== 'human')
|
||||
return undefined
|
||||
const content = m.message!.content
|
||||
let raw: string | undefined
|
||||
if (typeof content === 'string') {
|
||||
@@ -266,7 +267,13 @@ export function handleServerControlRequest(
|
||||
// Outbound-only: reply error for mutable requests so claude.ai doesn't show
|
||||
// false success. initialize must still succeed (server kills the connection
|
||||
// if it doesn't — see comment above).
|
||||
const req = request.request as { subtype: string; model?: string; max_thinking_tokens?: number | null; mode?: string; [key: string]: unknown }
|
||||
const req = request.request as {
|
||||
subtype: string
|
||||
model?: string
|
||||
max_thinking_tokens?: number | null
|
||||
mode?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
if (outboundOnly && req.subtype !== 'initialize') {
|
||||
response = {
|
||||
type: 'control_response',
|
||||
@@ -389,8 +396,8 @@ export function handleServerControlRequest(
|
||||
void transport.write(event)
|
||||
rcLog(
|
||||
`control_response: subtype=${req.subtype}` +
|
||||
` request_id=${request.request_id}` +
|
||||
` result=${(response.response as { subtype?: string }).subtype}`,
|
||||
` request_id=${request.request_id}` +
|
||||
` result=${(response.response as { subtype?: string }).subtype}`,
|
||||
)
|
||||
logForDebugging(
|
||||
`[bridge:repl] Sent control_response for ${req.subtype} request_id=${request.request_id} result=${(response.response as { subtype?: string }).subtype}`,
|
||||
|
||||
@@ -24,7 +24,9 @@ export function extractInboundMessageFields(
|
||||
| { content: string | Array<ContentBlockParam>; uuid: UUID | undefined }
|
||||
| undefined {
|
||||
if (msg.type !== 'user') return undefined
|
||||
const content = (msg.message as { content?: string | Array<ContentBlockParam> } | undefined)?.content
|
||||
const content = (
|
||||
msg.message as { content?: string | Array<ContentBlockParam> } | undefined
|
||||
)?.content
|
||||
if (!content) return undefined
|
||||
if (Array.isArray(content) && content.length === 0) return undefined
|
||||
|
||||
|
||||
@@ -290,7 +290,9 @@ export async function initReplBridge(
|
||||
isSyntheticMessage(msg)
|
||||
)
|
||||
continue
|
||||
const rawContent = getContentText(msg.message!.content as string | ContentBlockParam[])
|
||||
const rawContent = getContentText(
|
||||
msg.message!.content as string | ContentBlockParam[],
|
||||
)
|
||||
if (!rawContent) continue
|
||||
const derived = deriveTitle(rawContent)
|
||||
if (!derived) continue
|
||||
|
||||
@@ -20,7 +20,10 @@ export function rcLog(msg: string): void {
|
||||
try {
|
||||
if (!headerWritten) {
|
||||
ensureLogDir()
|
||||
appendFileSync(LOG_PATH, `\n===== RC-DEBUG session ${new Date().toISOString()} =====\n`)
|
||||
appendFileSync(
|
||||
LOG_PATH,
|
||||
`\n===== RC-DEBUG session ${new Date().toISOString()} =====\n`,
|
||||
)
|
||||
headerWritten = true
|
||||
}
|
||||
const ts = new Date().toISOString().slice(11, 23) // HH:mm:ss.SSS
|
||||
|
||||
@@ -834,7 +834,10 @@ export async function initEnvLessBridgeCore(
|
||||
for (const msg of filtered) {
|
||||
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
|
||||
}
|
||||
const events = filtered.map(m => ({ ...m, session_id: sessionId })) as StdoutMessage[]
|
||||
const events = filtered.map(m => ({
|
||||
...m,
|
||||
session_id: sessionId,
|
||||
})) as StdoutMessage[]
|
||||
void transport.writeBatch(events)
|
||||
},
|
||||
sendControlRequest(request: SDKControlRequest) {
|
||||
@@ -844,8 +847,14 @@ export async function initEnvLessBridgeCore(
|
||||
)
|
||||
return
|
||||
}
|
||||
const event: TransportMessage = { ...request, session_id: sessionId } as TransportMessage
|
||||
if ((request as { request?: { subtype?: string } }).request?.subtype === 'can_use_tool') {
|
||||
const event: TransportMessage = {
|
||||
...request,
|
||||
session_id: sessionId,
|
||||
} as TransportMessage
|
||||
if (
|
||||
(request as { request?: { subtype?: string } }).request?.subtype ===
|
||||
'can_use_tool'
|
||||
) {
|
||||
transport.reportState('requires_action')
|
||||
}
|
||||
void transport.write(event as StdoutMessage)
|
||||
@@ -860,7 +869,10 @@ export async function initEnvLessBridgeCore(
|
||||
)
|
||||
return
|
||||
}
|
||||
const event: TransportMessage = { ...response, session_id: sessionId } as TransportMessage
|
||||
const event: TransportMessage = {
|
||||
...response,
|
||||
session_id: sessionId,
|
||||
} as TransportMessage
|
||||
transport.reportState('running')
|
||||
void transport.write(event as StdoutMessage)
|
||||
logForDebugging('[remote-bridge] Sent control_response')
|
||||
|
||||
@@ -452,7 +452,6 @@ export async function initBridgeCore(
|
||||
// re-created after a connection loss.
|
||||
let currentSessionId: string
|
||||
|
||||
|
||||
if (reusedPriorSession && prior) {
|
||||
currentSessionId = prior.sessionId
|
||||
logForDebugging(
|
||||
@@ -632,9 +631,9 @@ export async function initBridgeCore(
|
||||
environmentRecreations++
|
||||
rcLog(
|
||||
`doReconnect: attempt=${environmentRecreations}/${MAX_ENVIRONMENT_RECREATIONS}` +
|
||||
` envId=${environmentId}` +
|
||||
` sessionId=${currentSessionId}` +
|
||||
` workId=${currentWorkId}`,
|
||||
` envId=${environmentId}` +
|
||||
` sessionId=${currentSessionId}` +
|
||||
` workId=${currentWorkId}`,
|
||||
)
|
||||
// Invalidate any in-flight v2 handshake — the environment is being
|
||||
// recreated, so a stale transport arriving post-reconnect would be
|
||||
@@ -846,7 +845,6 @@ export async function initBridgeCore(
|
||||
// UUIDs are scoped per-session on the server, so re-flushing is safe.
|
||||
previouslyFlushedUUIDs?.clear()
|
||||
|
||||
|
||||
// Reset the counter so independent reconnections hours apart don't
|
||||
// exhaust the limit — it guards against rapid consecutive failures,
|
||||
// not lifetime total.
|
||||
@@ -907,8 +905,8 @@ export async function initBridgeCore(
|
||||
function handleTransportPermanentClose(closeCode: number | undefined): void {
|
||||
rcLog(
|
||||
`handleTransportPermanentClose: code=${closeCode}` +
|
||||
` transport=${transport ? 'exists' : 'null'}` +
|
||||
` pollAborted=${pollController.signal.aborted}`,
|
||||
` transport=${transport ? 'exists' : 'null'}` +
|
||||
` pollAborted=${pollController.signal.aborted}`,
|
||||
)
|
||||
logForDebugging(
|
||||
`[bridge:repl] Transport permanently closed: code=${closeCode}`,
|
||||
@@ -1303,7 +1301,9 @@ export async function initBridgeCore(
|
||||
session_id: currentSessionId,
|
||||
})) as TransportMessage[]
|
||||
const dropsBefore = newTransport.droppedBatchCount
|
||||
void newTransport.writeBatch(events as StdoutMessage[]).then(() => {
|
||||
void newTransport
|
||||
.writeBatch(events as StdoutMessage[])
|
||||
.then(() => {
|
||||
// If any batch was dropped during this flush (SI down for
|
||||
// maxConsecutiveFailures attempts), flush() still resolved
|
||||
// normally but the events were NOT delivered. Don't mark
|
||||
@@ -1357,10 +1357,10 @@ export async function initBridgeCore(
|
||||
const parsed = JSON.parse(data)
|
||||
rcLog(
|
||||
`ingress: type=${parsed.type}` +
|
||||
`${parsed.type === 'control_request' ? ` subtype=${(parsed.request as Record<string, unknown>)?.subtype} request_id=${parsed.request_id}` : ''}` +
|
||||
`${parsed.type === 'control_response' ? ` subtype=${(parsed.response as Record<string, unknown>)?.subtype} request_id=${(parsed.response as Record<string, unknown>)?.request_id}` : ''}` +
|
||||
`${parsed.type === 'user' ? ` uuid=${parsed.uuid}` : ''}` +
|
||||
`${parsed.type === 'keep_alive' ? '' : ` len=${data.length}`}`,
|
||||
`${parsed.type === 'control_request' ? ` subtype=${(parsed.request as Record<string, unknown>)?.subtype} request_id=${parsed.request_id}` : ''}` +
|
||||
`${parsed.type === 'control_response' ? ` subtype=${(parsed.response as Record<string, unknown>)?.subtype} request_id=${(parsed.response as Record<string, unknown>)?.request_id}` : ''}` +
|
||||
`${parsed.type === 'user' ? ` uuid=${parsed.uuid}` : ''}` +
|
||||
`${parsed.type === 'keep_alive' ? '' : ` len=${data.length}`}`,
|
||||
)
|
||||
} catch {
|
||||
rcLog(`ingress (non-JSON): ${String(data).slice(0, 200)}`)
|
||||
@@ -1387,9 +1387,9 @@ export async function initBridgeCore(
|
||||
if (transport !== newTransport) return
|
||||
rcLog(
|
||||
`transport onClose: code=${closeCode}` +
|
||||
` connected=${newTransport.isConnectedStatus()}` +
|
||||
` state=${newTransport.getStateLabel()}` +
|
||||
` seq=${newTransport.getLastSequenceNum()}`,
|
||||
` connected=${newTransport.isConnectedStatus()}` +
|
||||
` state=${newTransport.getStateLabel()}` +
|
||||
` seq=${newTransport.getLastSequenceNum()}`,
|
||||
)
|
||||
handleTransportPermanentClose(closeCode)
|
||||
})
|
||||
@@ -1818,7 +1818,10 @@ export async function initBridgeCore(
|
||||
for (const msg of filtered) {
|
||||
if (msg.uuid) recentPostedUUIDs.add(msg.uuid as string)
|
||||
}
|
||||
const events: TransportMessage[] = filtered.map(m => ({ ...m, session_id: currentSessionId })) as TransportMessage[]
|
||||
const events: TransportMessage[] = filtered.map(m => ({
|
||||
...m,
|
||||
session_id: currentSessionId,
|
||||
})) as TransportMessage[]
|
||||
void transport.writeBatch(events as StdoutMessage[])
|
||||
},
|
||||
sendControlRequest(request: SDKControlRequest) {
|
||||
@@ -1828,7 +1831,10 @@ export async function initBridgeCore(
|
||||
)
|
||||
return
|
||||
}
|
||||
const event: TransportMessage = { ...request, session_id: currentSessionId } as TransportMessage
|
||||
const event: TransportMessage = {
|
||||
...request,
|
||||
session_id: currentSessionId,
|
||||
} as TransportMessage
|
||||
void transport.write(event as StdoutMessage)
|
||||
logForDebugging(
|
||||
`[bridge:repl] Sent control_request request_id=${request.request_id}`,
|
||||
@@ -1841,7 +1847,10 @@ export async function initBridgeCore(
|
||||
)
|
||||
return
|
||||
}
|
||||
const event: TransportMessage = { ...response, session_id: currentSessionId } as TransportMessage
|
||||
const event: TransportMessage = {
|
||||
...response,
|
||||
session_id: currentSessionId,
|
||||
} as TransportMessage
|
||||
void transport.write(event as StdoutMessage)
|
||||
logForDebugging('[bridge:repl] Sent control_response')
|
||||
},
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type StdoutMessage = any;
|
||||
export type StdoutMessage = any
|
||||
|
||||
@@ -11,21 +11,44 @@
|
||||
/** Patterns that match known secret/token formats. */
|
||||
const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
|
||||
// GitHub tokens (PAT, OAuth, App, Server-to-server)
|
||||
{ pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g, replacement: '[REDACTED_GITHUB_TOKEN]' },
|
||||
{
|
||||
pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g,
|
||||
replacement: '[REDACTED_GITHUB_TOKEN]',
|
||||
},
|
||||
// Anthropic API keys
|
||||
{ pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g, replacement: '[REDACTED_ANTHROPIC_KEY]' },
|
||||
{
|
||||
pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g,
|
||||
replacement: '[REDACTED_ANTHROPIC_KEY]',
|
||||
},
|
||||
// Generic Bearer tokens in headers
|
||||
{ pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi, replacement: '$1[REDACTED_TOKEN]' },
|
||||
{
|
||||
pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi,
|
||||
replacement: '$1[REDACTED_TOKEN]',
|
||||
},
|
||||
// AWS access keys
|
||||
{ pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED_AWS_KEY]' },
|
||||
{
|
||||
pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
||||
replacement: '[REDACTED_AWS_KEY]',
|
||||
},
|
||||
// AWS secret keys (40-char base64-like strings after common labels)
|
||||
{ pattern: /(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi, replacement: '$1=[REDACTED_AWS_SECRET]' },
|
||||
{
|
||||
pattern:
|
||||
/(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi,
|
||||
replacement: '$1=[REDACTED_AWS_SECRET]',
|
||||
},
|
||||
// Generic API key patterns (key=value or "key": "value")
|
||||
{ pattern: /(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi, replacement: '$1=[REDACTED]' },
|
||||
{
|
||||
pattern:
|
||||
/(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi,
|
||||
replacement: '$1=[REDACTED]',
|
||||
},
|
||||
// npm tokens
|
||||
{ pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' },
|
||||
// Slack tokens
|
||||
{ pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g, replacement: '[REDACTED_SLACK_TOKEN]' },
|
||||
{
|
||||
pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g,
|
||||
replacement: '[REDACTED_SLACK_TOKEN]',
|
||||
},
|
||||
]
|
||||
|
||||
/** Maximum content length before truncation (100KB). */
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import figures from 'figures'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js'
|
||||
import { Box, Text, stringWidth } from '@anthropic/ink'
|
||||
import { useAppState, useSetAppState } from '../state/AppState.js'
|
||||
import type { AppState } from '../state/AppStateStore.js'
|
||||
import { getGlobalConfig } from '../utils/config.js'
|
||||
import { isFullscreenActive } from '../utils/fullscreen.js'
|
||||
import type { Theme } from '../utils/theme.js'
|
||||
import { getCompanion } from './companion.js'
|
||||
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js'
|
||||
import { RARITY_COLORS } from './types.js'
|
||||
import { feature } from 'bun:bundle';
|
||||
import figures from 'figures';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { Box, Text, stringWidth } from '@anthropic/ink';
|
||||
import { useAppState, useSetAppState } from '../state/AppState.js';
|
||||
import type { AppState } from '../state/AppStateStore.js';
|
||||
import { getGlobalConfig } from '../utils/config.js';
|
||||
import { isFullscreenActive } from '../utils/fullscreen.js';
|
||||
import type { Theme } from '../utils/theme.js';
|
||||
import { getCompanion } from './companion.js';
|
||||
import { renderFace, renderSprite, spriteFrameCount } from './sprites.js';
|
||||
import { RARITY_COLORS } from './types.js';
|
||||
|
||||
const TICK_MS = 500
|
||||
const BUBBLE_SHOW = 20 // ticks → ~10s at 500ms
|
||||
const FADE_WINDOW = 6 // last ~3s the bubble dims so you know it's about to go
|
||||
const PET_BURST_MS = 2500 // how long hearts float after /buddy pet
|
||||
const TICK_MS = 500;
|
||||
const BUBBLE_SHOW = 20; // ticks → ~10s at 500ms
|
||||
const FADE_WINDOW = 6; // last ~3s the bubble dims so you know it's about to go
|
||||
const PET_BURST_MS = 2500; // how long hearts float after /buddy pet
|
||||
|
||||
// Idle sequence: mostly rest (frame 0), occasional fidget (frames 1-2), rare blink.
|
||||
// Sequence indices map to sprite frames; -1 means "blink on frame 0".
|
||||
const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0]
|
||||
const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0];
|
||||
|
||||
// Hearts float up-and-out over 5 ticks (~2.5s). Prepended above the sprite.
|
||||
const H = figures.heart
|
||||
const H = figures.heart;
|
||||
const PET_HEARTS = [
|
||||
` ${H} ${H} `,
|
||||
` ${H} ${H} ${H} `,
|
||||
` ${H} ${H} ${H} `,
|
||||
`${H} ${H} ${H} `,
|
||||
'· · · ',
|
||||
]
|
||||
];
|
||||
|
||||
function wrap(text: string, width: number): string[] {
|
||||
const words = text.split(' ')
|
||||
const lines: string[] = []
|
||||
let cur = ''
|
||||
const words = text.split(' ');
|
||||
const lines: string[] = [];
|
||||
let cur = '';
|
||||
for (const w of words) {
|
||||
if (cur.length + w.length + 1 > width && cur) {
|
||||
lines.push(cur)
|
||||
cur = w
|
||||
lines.push(cur);
|
||||
cur = w;
|
||||
} else {
|
||||
cur = cur ? `${cur} ${w}` : w
|
||||
cur = cur ? `${cur} ${w}` : w;
|
||||
}
|
||||
}
|
||||
if (cur) lines.push(cur)
|
||||
return lines
|
||||
if (cur) lines.push(cur);
|
||||
return lines;
|
||||
}
|
||||
|
||||
function SpeechBubble({
|
||||
@@ -53,40 +53,29 @@ function SpeechBubble({
|
||||
fading,
|
||||
tail,
|
||||
}: {
|
||||
text: string
|
||||
color: keyof Theme
|
||||
fading: boolean
|
||||
tail: 'down' | 'right'
|
||||
text: string;
|
||||
color: keyof Theme;
|
||||
fading: boolean;
|
||||
tail: 'down' | 'right';
|
||||
}): React.ReactNode {
|
||||
const lines = wrap(text, 30)
|
||||
const borderColor = fading ? 'inactive' : color
|
||||
const lines = wrap(text, 30);
|
||||
const borderColor = fading ? 'inactive' : color;
|
||||
const bubble = (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
paddingX={1}
|
||||
width={34}
|
||||
>
|
||||
<Box flexDirection="column" borderStyle="round" borderColor={borderColor} paddingX={1} width={34}>
|
||||
{lines.map((l, i) => (
|
||||
<Text
|
||||
key={i}
|
||||
italic
|
||||
dimColor={!fading}
|
||||
color={fading ? 'inactive' : undefined}
|
||||
>
|
||||
<Text key={i} italic dimColor={!fading} color={fading ? 'inactive' : undefined}>
|
||||
{l}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
if (tail === 'right') {
|
||||
return (
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
{bubble}
|
||||
<Text color={borderColor}>─</Text>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box flexDirection="column" alignItems="flex-end" marginRight={1}>
|
||||
@@ -96,18 +85,18 @@ function SpeechBubble({
|
||||
<Text color={borderColor}>╲</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const MIN_COLS_FOR_FULL_SPRITE = 100
|
||||
const SPRITE_BODY_WIDTH = 12
|
||||
const NAME_ROW_PAD = 2 // focused state wraps name in spaces: ` name `
|
||||
const SPRITE_PADDING_X = 2
|
||||
const BUBBLE_WIDTH = 36 // SpeechBubble box (34) + tail column
|
||||
const NARROW_QUIP_CAP = 24
|
||||
export const MIN_COLS_FOR_FULL_SPRITE = 100;
|
||||
const SPRITE_BODY_WIDTH = 12;
|
||||
const NAME_ROW_PAD = 2; // focused state wraps name in spaces: ` name `
|
||||
const SPRITE_PADDING_X = 2;
|
||||
const BUBBLE_WIDTH = 36; // SpeechBubble box (34) + tail column
|
||||
const NARROW_QUIP_CAP = 24;
|
||||
|
||||
function spriteColWidth(nameWidth: number): number {
|
||||
return Math.max(SPRITE_BODY_WIDTH, nameWidth + NAME_ROW_PAD)
|
||||
return Math.max(SPRITE_BODY_WIDTH, nameWidth + NAME_ROW_PAD);
|
||||
}
|
||||
|
||||
// Width the sprite area consumes. PromptInput subtracts this so text wraps
|
||||
@@ -115,89 +104,73 @@ function spriteColWidth(nameWidth: number): number {
|
||||
// width); in non-fullscreen it sits inline and needs BUBBLE_WIDTH more.
|
||||
// Narrow terminals: 0 — REPL.tsx stacks the one-liner on its own row
|
||||
// (above input in fullscreen, below in scrollback), so no reservation.
|
||||
export function companionReservedColumns(
|
||||
terminalColumns: number,
|
||||
speaking: boolean,
|
||||
): number {
|
||||
if (!feature('BUDDY')) return 0
|
||||
const companion = getCompanion()
|
||||
if (!companion || getGlobalConfig().companionMuted) return 0
|
||||
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0
|
||||
const nameWidth = stringWidth(companion.name)
|
||||
const bubble = speaking && !isFullscreenActive() ? BUBBLE_WIDTH : 0
|
||||
return spriteColWidth(nameWidth) + SPRITE_PADDING_X + bubble
|
||||
export function companionReservedColumns(terminalColumns: number, speaking: boolean): number {
|
||||
if (!feature('BUDDY')) return 0;
|
||||
const companion = getCompanion();
|
||||
if (!companion || getGlobalConfig().companionMuted) return 0;
|
||||
if (terminalColumns < MIN_COLS_FOR_FULL_SPRITE) return 0;
|
||||
const nameWidth = stringWidth(companion.name);
|
||||
const bubble = speaking && !isFullscreenActive() ? BUBBLE_WIDTH : 0;
|
||||
return spriteColWidth(nameWidth) + SPRITE_PADDING_X + bubble;
|
||||
}
|
||||
|
||||
export function CompanionSprite(): React.ReactNode {
|
||||
const reaction = useAppState(s => s.companionReaction)
|
||||
const petAt = useAppState(s => s.companionPetAt)
|
||||
const focused = useAppState(s => s.footerSelection === 'companion')
|
||||
const setAppState = useSetAppState()
|
||||
const { columns } = useTerminalSize()
|
||||
const [tick, setTick] = useState(0)
|
||||
const lastSpokeTick = useRef(0)
|
||||
const reaction = useAppState(s => s.companionReaction);
|
||||
const petAt = useAppState(s => s.companionPetAt);
|
||||
const focused = useAppState(s => s.footerSelection === 'companion');
|
||||
const setAppState = useSetAppState();
|
||||
const { columns } = useTerminalSize();
|
||||
const [tick, setTick] = useState(0);
|
||||
const lastSpokeTick = useRef(0);
|
||||
// Sync-during-render (not useEffect) so the first post-pet render already
|
||||
// has petStartTick=tick and petAge=0 — otherwise frame 0 is skipped.
|
||||
const [{ petStartTick, forPetAt }, setPetStart] = useState({
|
||||
petStartTick: 0,
|
||||
forPetAt: petAt,
|
||||
})
|
||||
});
|
||||
if (petAt !== forPetAt) {
|
||||
setPetStart({ petStartTick: tick, forPetAt: petAt })
|
||||
setPetStart({ petStartTick: tick, forPetAt: petAt });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(
|
||||
setT => setT((t: number) => t + 1),
|
||||
TICK_MS,
|
||||
setTick,
|
||||
)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
const timer = setInterval(setT => setT((t: number) => t + 1), TICK_MS, setTick);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reaction) return
|
||||
lastSpokeTick.current = tick
|
||||
if (!reaction) return;
|
||||
lastSpokeTick.current = tick;
|
||||
const timer = setTimeout(
|
||||
setA =>
|
||||
setA((prev: AppState) =>
|
||||
prev.companionReaction === undefined
|
||||
? prev
|
||||
: { ...prev, companionReaction: undefined },
|
||||
prev.companionReaction === undefined ? prev : { ...prev, companionReaction: undefined },
|
||||
),
|
||||
BUBBLE_SHOW * TICK_MS,
|
||||
setAppState,
|
||||
)
|
||||
return () => clearTimeout(timer)
|
||||
);
|
||||
return () => clearTimeout(timer);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- tick intentionally captured at reaction-change, not tracked
|
||||
}, [reaction, setAppState])
|
||||
}, [reaction, setAppState]);
|
||||
|
||||
if (!feature('BUDDY')) return null
|
||||
const companion = getCompanion()
|
||||
if (!companion || getGlobalConfig().companionMuted) return null
|
||||
if (!feature('BUDDY')) return null;
|
||||
const companion = getCompanion();
|
||||
if (!companion || getGlobalConfig().companionMuted) return null;
|
||||
|
||||
const color = RARITY_COLORS[companion.rarity]
|
||||
const colWidth = spriteColWidth(stringWidth(companion.name))
|
||||
const color = RARITY_COLORS[companion.rarity];
|
||||
const colWidth = spriteColWidth(stringWidth(companion.name));
|
||||
|
||||
const bubbleAge = reaction ? tick - lastSpokeTick.current : 0
|
||||
const fading =
|
||||
reaction !== undefined && bubbleAge >= BUBBLE_SHOW - FADE_WINDOW
|
||||
const bubbleAge = reaction ? tick - lastSpokeTick.current : 0;
|
||||
const fading = reaction !== undefined && bubbleAge >= BUBBLE_SHOW - FADE_WINDOW;
|
||||
|
||||
const petAge = petAt ? tick - petStartTick : Infinity
|
||||
const petting = petAge * TICK_MS < PET_BURST_MS
|
||||
const petAge = petAt ? tick - petStartTick : Infinity;
|
||||
const petting = petAge * TICK_MS < PET_BURST_MS;
|
||||
|
||||
// Narrow terminals: collapse to one-line face. When speaking, the quip
|
||||
// replaces the name beside the face (no room for a bubble).
|
||||
if (columns < MIN_COLS_FOR_FULL_SPRITE) {
|
||||
const quip =
|
||||
reaction && reaction.length > NARROW_QUIP_CAP
|
||||
? reaction.slice(0, NARROW_QUIP_CAP - 1) + '…'
|
||||
: reaction
|
||||
const label = quip
|
||||
? `"${quip}"`
|
||||
: focused
|
||||
? ` ${companion.name} `
|
||||
: companion.name
|
||||
reaction && reaction.length > NARROW_QUIP_CAP ? reaction.slice(0, NARROW_QUIP_CAP - 1) + '…' : reaction;
|
||||
const label = quip ? `"${quip}"` : focused ? ` ${companion.name} ` : companion.name;
|
||||
return (
|
||||
<Box paddingX={1} alignSelf="flex-end">
|
||||
<Text>
|
||||
@@ -210,44 +183,34 @@ export function CompanionSprite(): React.ReactNode {
|
||||
dimColor={!focused && !reaction}
|
||||
bold={focused}
|
||||
inverse={focused && !reaction}
|
||||
color={
|
||||
reaction
|
||||
? fading
|
||||
? 'inactive'
|
||||
: color
|
||||
: focused
|
||||
? color
|
||||
: undefined
|
||||
}
|
||||
color={reaction ? (fading ? 'inactive' : color) : focused ? color : undefined}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
const frameCount = spriteFrameCount(companion.species)
|
||||
const heartFrame = petting ? PET_HEARTS[petAge % PET_HEARTS.length] : null
|
||||
const frameCount = spriteFrameCount(companion.species);
|
||||
const heartFrame = petting ? PET_HEARTS[petAge % PET_HEARTS.length] : null;
|
||||
|
||||
let spriteFrame: number
|
||||
let blink = false
|
||||
let spriteFrame: number;
|
||||
let blink = false;
|
||||
if (reaction || petting) {
|
||||
// Excited: cycle all fidget frames fast
|
||||
spriteFrame = tick % frameCount
|
||||
spriteFrame = tick % frameCount;
|
||||
} else {
|
||||
const step = IDLE_SEQUENCE[tick % IDLE_SEQUENCE.length]!
|
||||
const step = IDLE_SEQUENCE[tick % IDLE_SEQUENCE.length]!;
|
||||
if (step === -1) {
|
||||
spriteFrame = 0
|
||||
blink = true
|
||||
spriteFrame = 0;
|
||||
blink = true;
|
||||
} else {
|
||||
spriteFrame = step % frameCount
|
||||
spriteFrame = step % frameCount;
|
||||
}
|
||||
}
|
||||
|
||||
const body = renderSprite(companion, spriteFrame).map(line =>
|
||||
blink ? line.replaceAll(companion.eye, '-') : line,
|
||||
)
|
||||
const sprite = heartFrame ? [heartFrame, ...body] : body
|
||||
const body = renderSprite(companion, spriteFrame).map(line => (blink ? line.replaceAll(companion.eye, '-') : line));
|
||||
const sprite = heartFrame ? [heartFrame, ...body] : body;
|
||||
|
||||
// Name row doubles as hint row — unfocused shows dim name + ↓ discovery,
|
||||
// focused shows inverse name. The enter-to-open hint lives in
|
||||
@@ -255,31 +218,20 @@ export function CompanionSprite(): React.ReactNode {
|
||||
// sprite doesn't jump up when selected. flexShrink=0 stops the
|
||||
// inline-bubble row wrapper from squeezing the sprite to fit.
|
||||
const spriteColumn = (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
flexShrink={0}
|
||||
alignItems="center"
|
||||
width={colWidth}
|
||||
>
|
||||
<Box flexDirection="column" flexShrink={0} alignItems="center" width={colWidth}>
|
||||
{sprite.map((line, i) => (
|
||||
<Text key={i} color={i === 0 && heartFrame ? 'autoAccept' : color}>
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
<Text
|
||||
italic
|
||||
bold={focused}
|
||||
dimColor={!focused}
|
||||
color={focused ? color : undefined}
|
||||
inverse={focused}
|
||||
>
|
||||
<Text italic bold={focused} dimColor={!focused} color={focused ? color : undefined} inverse={focused}>
|
||||
{focused ? ` ${companion.name} ` : companion.name}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
|
||||
if (!reaction) {
|
||||
return <Box paddingX={1}>{spriteColumn}</Box>
|
||||
return <Box paddingX={1}>{spriteColumn}</Box>;
|
||||
}
|
||||
|
||||
// Fullscreen: bubble renders separately via CompanionFloatingBubble in
|
||||
@@ -288,19 +240,14 @@ export function CompanionSprite(): React.ReactNode {
|
||||
// Non-fullscreen: bubble sits inline beside the sprite (input shrinks)
|
||||
// because floating into Static scrollback can't be cleared.
|
||||
if (isFullscreenActive()) {
|
||||
return <Box paddingX={1}>{spriteColumn}</Box>
|
||||
return <Box paddingX={1}>{spriteColumn}</Box>;
|
||||
}
|
||||
return (
|
||||
<Box flexDirection="row" alignItems="flex-end" paddingX={1} flexShrink={0}>
|
||||
<SpeechBubble
|
||||
text={reaction}
|
||||
color={color}
|
||||
fading={fading}
|
||||
tail="right"
|
||||
/>
|
||||
<SpeechBubble text={reaction} color={color} fading={fading} tail="right" />
|
||||
{spriteColumn}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Floating bubble overlay for fullscreen mode. Mounted in FullscreenLayout's
|
||||
@@ -308,33 +255,29 @@ export function CompanionSprite(): React.ReactNode {
|
||||
// the ScrollBox region. CompanionSprite owns the clear-after-10s timer; this
|
||||
// just reads companionReaction and renders the fade.
|
||||
export function CompanionFloatingBubble(): React.ReactNode {
|
||||
const reaction = useAppState(s => s.companionReaction)
|
||||
const reaction = useAppState(s => s.companionReaction);
|
||||
const [{ tick, forReaction }, setTick] = useState({
|
||||
tick: 0,
|
||||
forReaction: reaction,
|
||||
})
|
||||
});
|
||||
|
||||
// Reset tick synchronously when reaction changes (not in useEffect, which
|
||||
// runs post-render and would show one stale-faded frame). Storing the
|
||||
// reaction the tick is counting FOR alongside the tick itself means the
|
||||
// fade computation never sees a tick from a previous reaction.
|
||||
if (reaction !== forReaction) {
|
||||
setTick({ tick: 0, forReaction: reaction })
|
||||
setTick({ tick: 0, forReaction: reaction });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!reaction) return
|
||||
const timer = setInterval(
|
||||
set => set(s => ({ ...s, tick: s.tick + 1 })),
|
||||
TICK_MS,
|
||||
setTick,
|
||||
)
|
||||
return () => clearInterval(timer)
|
||||
}, [reaction])
|
||||
if (!reaction) return;
|
||||
const timer = setInterval(set => set(s => ({ ...s, tick: s.tick + 1 })), TICK_MS, setTick);
|
||||
return () => clearInterval(timer);
|
||||
}, [reaction]);
|
||||
|
||||
if (!feature('BUDDY') || !reaction) return null
|
||||
const companion = getCompanion()
|
||||
if (!companion || getGlobalConfig().companionMuted) return null
|
||||
if (!feature('BUDDY') || !reaction) return null;
|
||||
const companion = getCompanion();
|
||||
if (!companion || getGlobalConfig().companionMuted) return null;
|
||||
|
||||
return (
|
||||
<SpeechBubble
|
||||
@@ -343,5 +286,5 @@ export function CompanionFloatingBubble(): React.ReactNode {
|
||||
fading={tick >= BUBBLE_SHOW - FADE_WINDOW}
|
||||
tail="down"
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useNotifications } from '../context/notifications.js'
|
||||
import { Text } from '@anthropic/ink'
|
||||
import { getGlobalConfig } from '../utils/config.js'
|
||||
import { getRainbowColor } from '../utils/thinking.js'
|
||||
import { feature } from 'bun:bundle';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNotifications } from '../context/notifications.js';
|
||||
import { Text } from '@anthropic/ink';
|
||||
import { getGlobalConfig } from '../utils/config.js';
|
||||
import { getRainbowColor } from '../utils/thinking.js';
|
||||
|
||||
// Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter
|
||||
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
|
||||
// Teaser window: April 1-7, 2026 only. Command stays live forever after.
|
||||
export function isBuddyTeaserWindow(): boolean {
|
||||
if (process.env.USER_TYPE === 'ant') return true
|
||||
const d = new Date()
|
||||
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7
|
||||
if (process.env.USER_TYPE === 'ant') return true;
|
||||
const d = new Date();
|
||||
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
|
||||
}
|
||||
|
||||
export function isBuddyLive(): boolean {
|
||||
if (process.env.USER_TYPE === 'ant') return true
|
||||
const d = new Date()
|
||||
return (
|
||||
d.getFullYear() > 2026 || (d.getFullYear() === 2026 && d.getMonth() >= 3)
|
||||
)
|
||||
if (process.env.USER_TYPE === 'ant') return true;
|
||||
const d = new Date();
|
||||
return d.getFullYear() > 2026 || (d.getFullYear() === 2026 && d.getMonth() >= 3);
|
||||
}
|
||||
|
||||
function RainbowText({ text }: { text: string }): React.ReactNode {
|
||||
@@ -31,37 +29,35 @@ function RainbowText({ text }: { text: string }): React.ReactNode {
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Rainbow /buddy teaser shown on startup when no companion hatched yet.
|
||||
// Idle presence and reactions are handled by CompanionSprite directly.
|
||||
export function useBuddyNotification(): void {
|
||||
const { addNotification, removeNotification } = useNotifications()
|
||||
const { addNotification, removeNotification } = useNotifications();
|
||||
|
||||
useEffect(() => {
|
||||
if (!feature('BUDDY')) return
|
||||
const config = getGlobalConfig()
|
||||
if (config.companion || !isBuddyTeaserWindow()) return
|
||||
if (!feature('BUDDY')) return;
|
||||
const config = getGlobalConfig();
|
||||
if (config.companion || !isBuddyTeaserWindow()) return;
|
||||
addNotification({
|
||||
key: 'buddy-teaser',
|
||||
jsx: <RainbowText text="/buddy" />,
|
||||
priority: 'immediate',
|
||||
timeoutMs: 15_000,
|
||||
})
|
||||
return () => removeNotification('buddy-teaser')
|
||||
}, [addNotification, removeNotification])
|
||||
});
|
||||
return () => removeNotification('buddy-teaser');
|
||||
}, [addNotification, removeNotification]);
|
||||
}
|
||||
|
||||
export function findBuddyTriggerPositions(
|
||||
text: string,
|
||||
): Array<{ start: number; end: number }> {
|
||||
if (!feature('BUDDY')) return []
|
||||
const triggers: Array<{ start: number; end: number }> = []
|
||||
const re = /\/buddy\b/g
|
||||
let m: RegExpExecArray | null
|
||||
export function findBuddyTriggerPositions(text: string): Array<{ start: number; end: number }> {
|
||||
if (!feature('BUDDY')) return [];
|
||||
const triggers: Array<{ start: number; end: number }> = [];
|
||||
const re = /\/buddy\b/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
triggers.push({ start: m.index, end: m.index + m[0].length })
|
||||
triggers.push({ start: m.index, end: m.index + m[0].length });
|
||||
}
|
||||
return triggers
|
||||
return triggers;
|
||||
}
|
||||
|
||||
@@ -200,7 +200,9 @@ export async function attachHandler(target: string | undefined): Promise<void> {
|
||||
const { TmuxEngine } = await import('./bg/engines/tmux.js')
|
||||
const tmux = new TmuxEngine()
|
||||
if (!(await tmux.available())) {
|
||||
console.error('tmux is no longer available. Cannot attach to tmux session.')
|
||||
console.error(
|
||||
'tmux is no longer available. Cannot attach to tmux session.',
|
||||
)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
@@ -301,7 +303,9 @@ export async function handleBgStart(args: string[]): Promise<void> {
|
||||
console.log(` Engine: ${result.engineUsed}`)
|
||||
console.log(` Log: ${result.logPath}`)
|
||||
console.log()
|
||||
console.log(`Use \`claude daemon attach ${result.sessionName}\` to reconnect.`)
|
||||
console.log(
|
||||
`Use \`claude daemon attach ${result.sessionName}\` to reconnect.`,
|
||||
)
|
||||
console.log(`Use \`claude daemon status\` to check status.`)
|
||||
console.log(`Use \`claude daemon kill ${result.sessionName}\` to stop.`)
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { spawn } from 'child_process'
|
||||
import { openSync, closeSync, mkdirSync } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
import type { BgEngine, BgStartOptions, BgStartResult, SessionEntry } from '../engine.js'
|
||||
import type {
|
||||
BgEngine,
|
||||
BgStartOptions,
|
||||
BgStartResult,
|
||||
SessionEntry,
|
||||
} from '../engine.js'
|
||||
import { tailLog } from '../tail.js'
|
||||
|
||||
export class DetachedEngine implements BgEngine {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export type { BgEngine, BgStartOptions, BgStartResult, SessionEntry } from '../engine.js'
|
||||
export type {
|
||||
BgEngine,
|
||||
BgStartOptions,
|
||||
BgStartResult,
|
||||
SessionEntry,
|
||||
} from '../engine.js'
|
||||
|
||||
export async function selectEngine(): Promise<import('../engine.js').BgEngine> {
|
||||
if (process.platform === 'win32') {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { spawnSync } from 'child_process'
|
||||
import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'
|
||||
import { quote } from '../../../utils/bash/shellQuote.js'
|
||||
import type { BgEngine, BgStartOptions, BgStartResult, SessionEntry } from '../engine.js'
|
||||
import type {
|
||||
BgEngine,
|
||||
BgStartOptions,
|
||||
BgStartResult,
|
||||
SessionEntry,
|
||||
} from '../engine.js'
|
||||
|
||||
export class TmuxEngine implements BgEngine {
|
||||
readonly name = 'tmux' as const
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
/** Write an error message to stderr (if given) and exit with code 1. */
|
||||
export function cliError(msg?: string): never {
|
||||
// biome-ignore lint/suspicious/noConsole: centralized CLI error output
|
||||
if (msg) console.error(msg)
|
||||
process.exit(1)
|
||||
return undefined as never
|
||||
|
||||
@@ -59,12 +59,9 @@ export async function agentsHandler(): Promise<void> {
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('No agents found.')
|
||||
} else {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${totalActive} active agents\n`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(lines.join('\n').trimEnd())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,9 @@ export async function authLogin({
|
||||
|
||||
const orgResult = await validateForceLoginOrg()
|
||||
if (!orgResult.valid) {
|
||||
process.stderr.write((orgResult as { valid: false; message: string }).message + '\n')
|
||||
process.stderr.write(
|
||||
(orgResult as { valid: false; message: string }).message + '\n',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -209,7 +211,9 @@ export async function authLogin({
|
||||
|
||||
const orgResult = await validateForceLoginOrg()
|
||||
if (!orgResult.valid) {
|
||||
process.stderr.write((orgResult as { valid: false; message: string }).message + '\n')
|
||||
process.stderr.write(
|
||||
(orgResult as { valid: false; message: string }).message + '\n',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,203 +3,163 @@
|
||||
* These are dynamically imported only when the corresponding `claude mcp *` command runs.
|
||||
*/
|
||||
|
||||
import { stat } from 'fs/promises'
|
||||
import pMap from 'p-map'
|
||||
import { cwd } from 'process'
|
||||
import React from 'react'
|
||||
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js'
|
||||
import { wrappedRender as render } from '@anthropic/ink'
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
|
||||
import { stat } from 'fs/promises';
|
||||
import pMap from 'p-map';
|
||||
import { cwd } from 'process';
|
||||
import React from 'react';
|
||||
import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopImportDialog.js';
|
||||
import { wrappedRender as render } from '@anthropic/ink';
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js';
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
} from '../../services/analytics/index.js';
|
||||
import {
|
||||
clearMcpClientConfig,
|
||||
clearServerTokensFromLocalStorage,
|
||||
getMcpClientConfig,
|
||||
readClientSecret,
|
||||
saveMcpClientSecret,
|
||||
} from '../../services/mcp/auth.js'
|
||||
import {
|
||||
connectToServer,
|
||||
getMcpServerConnectionBatchSize,
|
||||
} from '../../services/mcp/client.js'
|
||||
} from '../../services/mcp/auth.js';
|
||||
import { connectToServer, getMcpServerConnectionBatchSize } from '../../services/mcp/client.js';
|
||||
import {
|
||||
addMcpConfig,
|
||||
getAllMcpConfigs,
|
||||
getMcpConfigByName,
|
||||
getMcpConfigsByScope,
|
||||
removeMcpConfig,
|
||||
} from '../../services/mcp/config.js'
|
||||
import type {
|
||||
ConfigScope,
|
||||
ScopedMcpServerConfig,
|
||||
} from '../../services/mcp/types.js'
|
||||
import {
|
||||
describeMcpConfigFilePath,
|
||||
ensureConfigScope,
|
||||
getScopeLabel,
|
||||
} from '../../services/mcp/utils.js'
|
||||
import { AppStateProvider } from '../../state/AppState.js'
|
||||
import {
|
||||
getCurrentProjectConfig,
|
||||
getGlobalConfig,
|
||||
saveCurrentProjectConfig,
|
||||
} from '../../utils/config.js'
|
||||
import { isFsInaccessible } from '../../utils/errors.js'
|
||||
import { gracefulShutdown } from '../../utils/gracefulShutdown.js'
|
||||
import { safeParseJSON } from '../../utils/json.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { cliError, cliOk } from '../exit.js'
|
||||
} from '../../services/mcp/config.js';
|
||||
import type { ConfigScope, ScopedMcpServerConfig } from '../../services/mcp/types.js';
|
||||
import { describeMcpConfigFilePath, ensureConfigScope, getScopeLabel } from '../../services/mcp/utils.js';
|
||||
import { AppStateProvider } from '../../state/AppState.js';
|
||||
import { getCurrentProjectConfig, getGlobalConfig, saveCurrentProjectConfig } from '../../utils/config.js';
|
||||
import { isFsInaccessible } from '../../utils/errors.js';
|
||||
import { gracefulShutdown } from '../../utils/gracefulShutdown.js';
|
||||
import { safeParseJSON } from '../../utils/json.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import { cliError, cliOk } from '../exit.js';
|
||||
|
||||
async function checkMcpServerHealth(
|
||||
name: string,
|
||||
server: ScopedMcpServerConfig,
|
||||
): Promise<string> {
|
||||
async function checkMcpServerHealth(name: string, server: ScopedMcpServerConfig): Promise<string> {
|
||||
try {
|
||||
const result = await connectToServer(name, server)
|
||||
const result = await connectToServer(name, server);
|
||||
if (result.type === 'connected') {
|
||||
return '✓ Connected'
|
||||
return '✓ Connected';
|
||||
} else if (result.type === 'needs-auth') {
|
||||
return '! Needs authentication'
|
||||
return '! Needs authentication';
|
||||
} else {
|
||||
return '✗ Failed to connect'
|
||||
return '✗ Failed to connect';
|
||||
}
|
||||
} catch (_error) {
|
||||
return '✗ Connection error'
|
||||
return '✗ Connection error';
|
||||
}
|
||||
}
|
||||
|
||||
// mcp serve (lines 4512–4532)
|
||||
export async function mcpServeHandler({
|
||||
debug,
|
||||
verbose,
|
||||
}: {
|
||||
debug?: boolean
|
||||
verbose?: boolean
|
||||
}): Promise<void> {
|
||||
const providedCwd = cwd()
|
||||
logEvent('tengu_mcp_start', {})
|
||||
export async function mcpServeHandler({ debug, verbose }: { debug?: boolean; verbose?: boolean }): Promise<void> {
|
||||
const providedCwd = cwd();
|
||||
logEvent('tengu_mcp_start', {});
|
||||
|
||||
try {
|
||||
await stat(providedCwd)
|
||||
await stat(providedCwd);
|
||||
} catch (error) {
|
||||
if (isFsInaccessible(error)) {
|
||||
cliError(`Error: Directory ${providedCwd} does not exist`)
|
||||
cliError(`Error: Directory ${providedCwd} does not exist`);
|
||||
}
|
||||
throw error
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const { setup } = await import('../../setup.js')
|
||||
await setup(providedCwd, 'default', false, false, undefined, false)
|
||||
const { startMCPServer } = await import('../../entrypoints/mcp.js')
|
||||
await startMCPServer(providedCwd, debug ?? false, verbose ?? false)
|
||||
const { setup } = await import('../../setup.js');
|
||||
await setup(providedCwd, 'default', false, false, undefined, false);
|
||||
const { startMCPServer } = await import('../../entrypoints/mcp.js');
|
||||
await startMCPServer(providedCwd, debug ?? false, verbose ?? false);
|
||||
} catch (error) {
|
||||
cliError(`Error: Failed to start MCP server: ${error}`)
|
||||
cliError(`Error: Failed to start MCP server: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// mcp remove (lines 4545–4635)
|
||||
export async function mcpRemoveHandler(
|
||||
name: string,
|
||||
options: { scope?: string },
|
||||
): Promise<void> {
|
||||
export async function mcpRemoveHandler(name: string, options: { scope?: string }): Promise<void> {
|
||||
// Look up config before removing so we can clean up secure storage
|
||||
const serverBeforeRemoval = getMcpConfigByName(name)
|
||||
const serverBeforeRemoval = getMcpConfigByName(name);
|
||||
|
||||
const cleanupSecureStorage = () => {
|
||||
if (
|
||||
serverBeforeRemoval &&
|
||||
(serverBeforeRemoval.type === 'sse' ||
|
||||
serverBeforeRemoval.type === 'http')
|
||||
) {
|
||||
clearServerTokensFromLocalStorage(name, serverBeforeRemoval)
|
||||
clearMcpClientConfig(name, serverBeforeRemoval)
|
||||
if (serverBeforeRemoval && (serverBeforeRemoval.type === 'sse' || serverBeforeRemoval.type === 'http')) {
|
||||
clearServerTokensFromLocalStorage(name, serverBeforeRemoval);
|
||||
clearMcpClientConfig(name, serverBeforeRemoval);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (options.scope) {
|
||||
const scope = ensureConfigScope(options.scope)
|
||||
const scope = ensureConfigScope(options.scope);
|
||||
logEvent('tengu_mcp_delete', {
|
||||
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
});
|
||||
|
||||
await removeMcpConfig(name, scope)
|
||||
cleanupSecureStorage()
|
||||
process.stdout.write(`Removed MCP server ${name} from ${scope} config\n`)
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`)
|
||||
await removeMcpConfig(name, scope);
|
||||
cleanupSecureStorage();
|
||||
process.stdout.write(`Removed MCP server ${name} from ${scope} config\n`);
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`);
|
||||
}
|
||||
|
||||
// If no scope specified, check where the server exists
|
||||
const projectConfig = getCurrentProjectConfig()
|
||||
const globalConfig = getGlobalConfig()
|
||||
const projectConfig = getCurrentProjectConfig();
|
||||
const globalConfig = getGlobalConfig();
|
||||
|
||||
// Check if server exists in project scope (.mcp.json)
|
||||
const { servers: projectServers } = getMcpConfigsByScope('project')
|
||||
const mcpJsonExists = !!projectServers[name]
|
||||
const { servers: projectServers } = getMcpConfigsByScope('project');
|
||||
const mcpJsonExists = !!projectServers[name];
|
||||
|
||||
// Count how many scopes contain this server
|
||||
const scopes: Array<Exclude<ConfigScope, 'dynamic'>> = []
|
||||
if (projectConfig.mcpServers?.[name]) scopes.push('local')
|
||||
if (mcpJsonExists) scopes.push('project')
|
||||
if (globalConfig.mcpServers?.[name]) scopes.push('user')
|
||||
const scopes: Array<Exclude<ConfigScope, 'dynamic'>> = [];
|
||||
if (projectConfig.mcpServers?.[name]) scopes.push('local');
|
||||
if (mcpJsonExists) scopes.push('project');
|
||||
if (globalConfig.mcpServers?.[name]) scopes.push('user');
|
||||
|
||||
if (scopes.length === 0) {
|
||||
cliError(`No MCP server found with name: "${name}"`)
|
||||
cliError(`No MCP server found with name: "${name}"`);
|
||||
} else if (scopes.length === 1) {
|
||||
// Server exists in only one scope, remove it
|
||||
const scope = scopes[0]!
|
||||
const scope = scopes[0]!;
|
||||
logEvent('tengu_mcp_delete', {
|
||||
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
});
|
||||
|
||||
await removeMcpConfig(name, scope)
|
||||
cleanupSecureStorage()
|
||||
process.stdout.write(
|
||||
`Removed MCP server "${name}" from ${scope} config\n`,
|
||||
)
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`)
|
||||
await removeMcpConfig(name, scope);
|
||||
cleanupSecureStorage();
|
||||
process.stdout.write(`Removed MCP server "${name}" from ${scope} config\n`);
|
||||
cliOk(`File modified: ${describeMcpConfigFilePath(scope)}`);
|
||||
} else {
|
||||
// Server exists in multiple scopes
|
||||
process.stderr.write(`MCP server "${name}" exists in multiple scopes:\n`)
|
||||
process.stderr.write(`MCP server "${name}" exists in multiple scopes:\n`);
|
||||
scopes.forEach(scope => {
|
||||
process.stderr.write(
|
||||
` - ${getScopeLabel(scope)} (${describeMcpConfigFilePath(scope)})\n`,
|
||||
)
|
||||
})
|
||||
process.stderr.write('\nTo remove from a specific scope, use:\n')
|
||||
process.stderr.write(` - ${getScopeLabel(scope)} (${describeMcpConfigFilePath(scope)})\n`);
|
||||
});
|
||||
process.stderr.write('\nTo remove from a specific scope, use:\n');
|
||||
scopes.forEach(scope => {
|
||||
process.stderr.write(` claude mcp remove "${name}" -s ${scope}\n`)
|
||||
})
|
||||
cliError()
|
||||
process.stderr.write(` claude mcp remove "${name}" -s ${scope}\n`);
|
||||
});
|
||||
cliError();
|
||||
}
|
||||
} catch (error) {
|
||||
cliError((error as Error).message)
|
||||
cliError((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// mcp list (lines 4641–4688)
|
||||
export async function mcpListHandler(): Promise<void> {
|
||||
logEvent('tengu_mcp_list', {})
|
||||
const { servers: configs } = await getAllMcpConfigs()
|
||||
logEvent('tengu_mcp_list', {});
|
||||
const { servers: configs } = await getAllMcpConfigs();
|
||||
if (Object.keys(configs).length === 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
'No MCP servers configured. Use `claude mcp add` to add a server.',
|
||||
)
|
||||
console.log('No MCP servers configured. Use `claude mcp add` to add a server.');
|
||||
} else {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Checking MCP server health...\n')
|
||||
console.log('Checking MCP server health...\n');
|
||||
|
||||
// Check servers concurrently
|
||||
const entries = Object.entries(configs)
|
||||
const entries = Object.entries(configs);
|
||||
const results = await pMap(
|
||||
entries,
|
||||
async ([name, server]) => ({
|
||||
@@ -208,127 +168,100 @@ export async function mcpListHandler(): Promise<void> {
|
||||
status: await checkMcpServerHealth(name, server),
|
||||
}),
|
||||
{ concurrency: getMcpServerConnectionBatchSize() },
|
||||
)
|
||||
);
|
||||
|
||||
for (const { name, server, status } of results) {
|
||||
// Intentionally excluding sse-ide servers here since they're internal
|
||||
if (server.type === 'sse') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}: ${server.url} (SSE) - ${status}`)
|
||||
console.log(`${name}: ${server.url} (SSE) - ${status}`);
|
||||
} else if (server.type === 'http') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}: ${server.url} (HTTP) - ${status}`)
|
||||
console.log(`${name}: ${server.url} (HTTP) - ${status}`);
|
||||
} else if (server.type === 'claudeai-proxy') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}: ${server.url} - ${status}`)
|
||||
console.log(`${name}: ${server.url} - ${status}`);
|
||||
} else if (!server.type || server.type === 'stdio') {
|
||||
const stdioServer = server as { command: string; args: string[]; type?: string }
|
||||
const args = Array.isArray(stdioServer.args) ? stdioServer.args : []
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}: ${stdioServer.command} ${args.join(' ')} - ${status}`)
|
||||
const stdioServer = server as { command: string; args: string[]; type?: string };
|
||||
const args = Array.isArray(stdioServer.args) ? stdioServer.args : [];
|
||||
console.log(`${name}: ${stdioServer.command} ${args.join(' ')} - ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use gracefulShutdown to properly clean up MCP server connections
|
||||
// (process.exit bypasses cleanup handlers, leaving child processes orphaned)
|
||||
await gracefulShutdown(0)
|
||||
await gracefulShutdown(0);
|
||||
}
|
||||
|
||||
// mcp get (lines 4694–4786)
|
||||
export async function mcpGetHandler(name: string): Promise<void> {
|
||||
logEvent('tengu_mcp_get', {
|
||||
name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
const server = getMcpConfigByName(name)
|
||||
});
|
||||
const server = getMcpConfigByName(name);
|
||||
if (!server) {
|
||||
cliError(`No MCP server found with name: ${name}`)
|
||||
cliError(`No MCP server found with name: ${name}`);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${name}:`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Scope: ${getScopeLabel(server.scope)}`)
|
||||
console.log(`${name}:`);
|
||||
console.log(` Scope: ${getScopeLabel(server.scope)}`);
|
||||
|
||||
// Check server health
|
||||
const status = await checkMcpServerHealth(name, server)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Status: ${status}`)
|
||||
const status = await checkMcpServerHealth(name, server);
|
||||
console.log(` Status: ${status}`);
|
||||
|
||||
// Intentionally excluding sse-ide servers here since they're internal
|
||||
if (server.type === 'sse') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Type: sse`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` URL: ${server.url}`)
|
||||
console.log(` Type: sse`);
|
||||
console.log(` URL: ${server.url}`);
|
||||
if (server.headers) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(' Headers:')
|
||||
console.log(' Headers:');
|
||||
for (const [key, value] of Object.entries(server.headers)) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${key}: ${value}`)
|
||||
console.log(` ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
if (server.oauth?.clientId || server.oauth?.callbackPort) {
|
||||
const parts: string[] = []
|
||||
const parts: string[] = [];
|
||||
if (server.oauth.clientId) {
|
||||
parts.push('client_id configured')
|
||||
const clientConfig = getMcpClientConfig(name, server)
|
||||
if (clientConfig?.clientSecret) parts.push('client_secret configured')
|
||||
parts.push('client_id configured');
|
||||
const clientConfig = getMcpClientConfig(name, server);
|
||||
if (clientConfig?.clientSecret) parts.push('client_secret configured');
|
||||
}
|
||||
if (server.oauth.callbackPort)
|
||||
parts.push(`callback_port ${server.oauth.callbackPort}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` OAuth: ${parts.join(', ')}`)
|
||||
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
|
||||
console.log(` OAuth: ${parts.join(', ')}`);
|
||||
}
|
||||
} else if (server.type === 'http') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Type: http`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` URL: ${server.url}`)
|
||||
console.log(` Type: http`);
|
||||
console.log(` URL: ${server.url}`);
|
||||
if (server.headers) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(' Headers:')
|
||||
console.log(' Headers:');
|
||||
for (const [key, value] of Object.entries(server.headers)) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${key}: ${value}`)
|
||||
console.log(` ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
if (server.oauth?.clientId || server.oauth?.callbackPort) {
|
||||
const parts: string[] = []
|
||||
const parts: string[] = [];
|
||||
if (server.oauth.clientId) {
|
||||
parts.push('client_id configured')
|
||||
const clientConfig = getMcpClientConfig(name, server)
|
||||
if (clientConfig?.clientSecret) parts.push('client_secret configured')
|
||||
parts.push('client_id configured');
|
||||
const clientConfig = getMcpClientConfig(name, server);
|
||||
if (clientConfig?.clientSecret) parts.push('client_secret configured');
|
||||
}
|
||||
if (server.oauth.callbackPort)
|
||||
parts.push(`callback_port ${server.oauth.callbackPort}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` OAuth: ${parts.join(', ')}`)
|
||||
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
|
||||
console.log(` OAuth: ${parts.join(', ')}`);
|
||||
}
|
||||
} else if (server.type === 'stdio') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Type: stdio`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Command: ${server.command}`)
|
||||
const args = Array.isArray(server.args) ? server.args : []
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Args: ${args.join(' ')}`)
|
||||
console.log(` Type: stdio`);
|
||||
console.log(` Command: ${server.command}`);
|
||||
const args = Array.isArray(server.args) ? server.args : [];
|
||||
console.log(` Args: ${args.join(' ')}`);
|
||||
if (server.env) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(' Environment:')
|
||||
console.log(' Environment:');
|
||||
for (const [key, value] of Object.entries(server.env)) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${key}=${value}`)
|
||||
console.log(` ${key}=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
`\nTo remove this server, run: claude mcp remove "${name}" -s ${server.scope}`,
|
||||
)
|
||||
console.log(`\nTo remove this server, run: claude mcp remove "${name}" -s ${server.scope}`);
|
||||
// Use gracefulShutdown to properly clean up MCP server connections
|
||||
// (process.exit bypasses cleanup handlers, leaving child processes orphaned)
|
||||
await gracefulShutdown(0)
|
||||
await gracefulShutdown(0);
|
||||
}
|
||||
|
||||
// mcp add-json (lines 4801–4870)
|
||||
@@ -338,8 +271,8 @@ export async function mcpAddJsonHandler(
|
||||
options: { scope?: string; clientSecret?: true },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const scope = ensureConfigScope(options.scope)
|
||||
const parsedJson = safeParseJSON(json)
|
||||
const scope = ensureConfigScope(options.scope);
|
||||
const parsedJson = safeParseJSON(json);
|
||||
|
||||
// Read secret before writing config so cancellation doesn't leave partial state
|
||||
const needsSecret =
|
||||
@@ -353,15 +286,15 @@ export async function mcpAddJsonHandler(
|
||||
'oauth' in parsedJson &&
|
||||
parsedJson.oauth &&
|
||||
typeof parsedJson.oauth === 'object' &&
|
||||
'clientId' in parsedJson.oauth
|
||||
const clientSecret = needsSecret ? await readClientSecret() : undefined
|
||||
'clientId' in parsedJson.oauth;
|
||||
const clientSecret = needsSecret ? await readClientSecret() : undefined;
|
||||
|
||||
await addMcpConfig(name, parsedJson, scope)
|
||||
await addMcpConfig(name, parsedJson, scope);
|
||||
|
||||
const transportType =
|
||||
parsedJson && typeof parsedJson === 'object' && 'type' in parsedJson
|
||||
? String(parsedJson.type || 'stdio')
|
||||
: 'stdio'
|
||||
: 'stdio';
|
||||
|
||||
if (
|
||||
clientSecret &&
|
||||
@@ -372,53 +305,38 @@ export async function mcpAddJsonHandler(
|
||||
'url' in parsedJson &&
|
||||
typeof parsedJson.url === 'string'
|
||||
) {
|
||||
saveMcpClientSecret(
|
||||
name,
|
||||
{ type: parsedJson.type, url: parsedJson.url },
|
||||
clientSecret,
|
||||
)
|
||||
saveMcpClientSecret(name, { type: parsedJson.type, url: parsedJson.url }, clientSecret);
|
||||
}
|
||||
|
||||
logEvent('tengu_mcp_add', {
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source:
|
||||
'json' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source: 'json' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
type: transportType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
});
|
||||
|
||||
cliOk(`Added ${transportType} MCP server ${name} to ${scope} config`)
|
||||
cliOk(`Added ${transportType} MCP server ${name} to ${scope} config`);
|
||||
} catch (error) {
|
||||
cliError((error as Error).message)
|
||||
cliError((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// mcp add-from-claude-desktop (lines 4881–4927)
|
||||
export async function mcpAddFromDesktopHandler(options: {
|
||||
scope?: string
|
||||
}): Promise<void> {
|
||||
export async function mcpAddFromDesktopHandler(options: { scope?: string }): Promise<void> {
|
||||
try {
|
||||
const scope = ensureConfigScope(options.scope)
|
||||
const platform = getPlatform()
|
||||
const scope = ensureConfigScope(options.scope);
|
||||
const platform = getPlatform();
|
||||
|
||||
logEvent('tengu_mcp_add', {
|
||||
scope:
|
||||
scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
platform:
|
||||
platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source:
|
||||
'desktop' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
platform: platform as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
source: 'desktop' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
});
|
||||
|
||||
const { readClaudeDesktopMcpServers } = await import(
|
||||
'../../utils/claudeDesktop.js'
|
||||
)
|
||||
const servers = await readClaudeDesktopMcpServers()
|
||||
const { readClaudeDesktopMcpServers } = await import('../../utils/claudeDesktop.js');
|
||||
const servers = await readClaudeDesktopMcpServers();
|
||||
|
||||
if (Object.keys(servers).length === 0) {
|
||||
cliOk(
|
||||
'No MCP servers found in Claude Desktop configuration or configuration file does not exist.',
|
||||
)
|
||||
cliOk('No MCP servers found in Claude Desktop configuration or configuration file does not exist.');
|
||||
}
|
||||
|
||||
const { unmount } = await render(
|
||||
@@ -428,29 +346,29 @@ export async function mcpAddFromDesktopHandler(options: {
|
||||
servers={servers}
|
||||
scope={scope}
|
||||
onDone={() => {
|
||||
unmount()
|
||||
unmount();
|
||||
}}
|
||||
/>
|
||||
</KeybindingSetup>
|
||||
</AppStateProvider>,
|
||||
{ exitOnCtrlC: true },
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
cliError((error as Error).message)
|
||||
cliError((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// mcp reset-project-choices (lines 4935–4952)
|
||||
export async function mcpResetChoicesHandler(): Promise<void> {
|
||||
logEvent('tengu_mcp_reset_mcpjson_choices', {})
|
||||
logEvent('tengu_mcp_reset_mcpjson_choices', {});
|
||||
saveCurrentProjectConfig(current => ({
|
||||
...current,
|
||||
enabledMcpjsonServers: [],
|
||||
disabledMcpjsonServers: [],
|
||||
enableAllProjectMcpServers: false,
|
||||
}))
|
||||
}));
|
||||
cliOk(
|
||||
'All project-scoped (.mcp.json) server approvals and rejections have been reset.\n' +
|
||||
'You will be prompted for approval next time you start Claude Code.',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,27 +72,21 @@ export function handleMarketplaceError(error: unknown, action: string): never {
|
||||
|
||||
function printValidationResult(result: ValidationResult): void {
|
||||
if (result.errors.length > 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
`${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n`,
|
||||
)
|
||||
result.errors.forEach(error => {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${error.path}: ${error.message}`)
|
||||
})
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
`${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n`,
|
||||
)
|
||||
result.warnings.forEach(warning => {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${warning.path}: ${warning.message}`)
|
||||
})
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
@@ -106,7 +100,6 @@ export async function pluginValidateHandler(
|
||||
try {
|
||||
const result = await validateManifest(manifestPath)
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Validating ${result.fileType} manifest: ${result.filePath}\n`)
|
||||
printValidationResult(result)
|
||||
|
||||
@@ -120,7 +113,6 @@ export async function pluginValidateHandler(
|
||||
if (basename(manifestDir) === '.claude-plugin') {
|
||||
contentResults = await validatePluginContents(dirname(manifestDir))
|
||||
for (const r of contentResults) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Validating ${r.fileType}: ${r.filePath}\n`)
|
||||
printValidationResult(r)
|
||||
}
|
||||
@@ -139,13 +131,11 @@ export async function pluginValidateHandler(
|
||||
: `${figures.tick} Validation passed`,
|
||||
)
|
||||
} else {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${figures.cross} Validation failed`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`,
|
||||
)
|
||||
@@ -358,7 +348,6 @@ export async function pluginListHandler(options: {
|
||||
}
|
||||
|
||||
if (pluginIds.length > 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Installed plugins:\n')
|
||||
}
|
||||
|
||||
@@ -383,25 +372,18 @@ export async function pluginListHandler(options: {
|
||||
const version = installation.version || 'unknown'
|
||||
const scope = installation.scope
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${pluginId}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Version: ${version}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Scope: ${scope}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Status: ${status}`)
|
||||
for (const error of pluginErrors) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Error: ${getPluginErrorMessage(error)}`)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
|
||||
if (inlinePlugins.length > 0 || inlineLoadErrors.length > 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Session-only plugins (--plugin-dir):\n')
|
||||
for (const p of inlinePlugins) {
|
||||
// Same dirName≠manifestName fallback as the JSON path above — error
|
||||
@@ -413,19 +395,13 @@ export async function pluginListHandler(options: {
|
||||
pErrors.length > 0
|
||||
? `${figures.cross} loaded with errors`
|
||||
: `${figures.tick} loaded`
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${p.source}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Version: ${p.manifest.version ?? 'unknown'}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Path: ${p.path}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Status: ${status}`)
|
||||
for (const e of pErrors) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Error: ${getPluginErrorMessage(e)}`)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
}
|
||||
// Path-level failures: no LoadedPlugin object exists. Show them so
|
||||
@@ -433,7 +409,6 @@ export async function pluginListHandler(options: {
|
||||
for (const e of inlineLoadErrors.filter(e =>
|
||||
e.source.startsWith('inline['),
|
||||
)) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
` ${figures.pointer} ${e.source}: ${figures.cross} ${getPluginErrorMessage(e)}\n`,
|
||||
)
|
||||
@@ -489,12 +464,10 @@ export async function marketplaceAddHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Adding marketplace...')
|
||||
|
||||
const { name, alreadyMaterialized, resolvedSource } =
|
||||
await addMarketplaceSource(marketplaceSource, message => {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(message)
|
||||
})
|
||||
|
||||
@@ -555,33 +528,25 @@ export async function marketplaceListHandler(options: {
|
||||
cliOk('No marketplaces configured')
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Configured marketplaces:\n')
|
||||
names.forEach(name => {
|
||||
const marketplace = config[name]
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${name}`)
|
||||
|
||||
if (marketplace?.source) {
|
||||
const src = marketplace.source
|
||||
if (src.source === 'github') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: GitHub (${src.repo})`)
|
||||
} else if (src.source === 'git') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: Git (${src.url})`)
|
||||
} else if (src.source === 'url') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: URL (${src.url})`)
|
||||
} else if (src.source === 'directory') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: Directory (${src.path})`)
|
||||
} else if (src.source === 'file') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: File (${src.path})`)
|
||||
}
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
})
|
||||
|
||||
@@ -620,11 +585,9 @@ export async function marketplaceUpdateHandler(
|
||||
if (options.cowork) setUseCoworkPlugins(true)
|
||||
try {
|
||||
if (name) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Updating marketplace: ${name}...`)
|
||||
|
||||
await refreshMarketplace(name, message => {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(message)
|
||||
})
|
||||
|
||||
@@ -644,7 +607,6 @@ export async function marketplaceUpdateHandler(
|
||||
cliOk('No marketplaces configured')
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Updating ${marketplaceNames.length} marketplace(s)...`)
|
||||
|
||||
await refreshAllMarketplaces()
|
||||
|
||||
@@ -4,26 +4,24 @@
|
||||
*/
|
||||
/* eslint-disable custom-rules/no-process-exit -- CLI subcommand handlers intentionally exit */
|
||||
|
||||
import { cwd } from 'process'
|
||||
import React from 'react'
|
||||
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js'
|
||||
import { useManagePlugins } from '../../hooks/useManagePlugins.js'
|
||||
import type { Root } from '@anthropic/ink'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js'
|
||||
import { AppStateProvider } from '../../state/AppState.js'
|
||||
import { onChangeAppState } from '../../state/onChangeAppState.js'
|
||||
import { isAnthropicAuthEnabled } from '../../utils/auth.js'
|
||||
import { cwd } from 'process';
|
||||
import React from 'react';
|
||||
import { WelcomeV2 } from '../../components/LogoV2/WelcomeV2.js';
|
||||
import { useManagePlugins } from '../../hooks/useManagePlugins.js';
|
||||
import type { Root } from '@anthropic/ink';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import { MCPConnectionManager } from '../../services/mcp/MCPConnectionManager.js';
|
||||
import { AppStateProvider } from '../../state/AppState.js';
|
||||
import { onChangeAppState } from '../../state/onChangeAppState.js';
|
||||
import { isAnthropicAuthEnabled } from '../../utils/auth.js';
|
||||
|
||||
export async function setupTokenHandler(root: Root): Promise<void> {
|
||||
logEvent('tengu_setup_token_command', {})
|
||||
logEvent('tengu_setup_token_command', {});
|
||||
|
||||
const showAuthWarning = !isAnthropicAuthEnabled()
|
||||
const { ConsoleOAuthFlow } = await import(
|
||||
'../../components/ConsoleOAuthFlow.js'
|
||||
)
|
||||
const showAuthWarning = !isAnthropicAuthEnabled();
|
||||
const { ConsoleOAuthFlow } = await import('../../components/ConsoleOAuthFlow.js');
|
||||
await new Promise<void>(resolve => {
|
||||
root.render(
|
||||
<AppStateProvider onChangeAppState={onChangeAppState}>
|
||||
@@ -33,18 +31,16 @@ export async function setupTokenHandler(root: Root): Promise<void> {
|
||||
{showAuthWarning && (
|
||||
<Box flexDirection="column">
|
||||
<Text color="warning">
|
||||
Warning: You already have authentication configured via
|
||||
environment variable or API key helper.
|
||||
Warning: You already have authentication configured via environment variable or API key helper.
|
||||
</Text>
|
||||
<Text color="warning">
|
||||
The setup-token command will create a new OAuth token which
|
||||
you can use instead.
|
||||
The setup-token command will create a new OAuth token which you can use instead.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<ConsoleOAuthFlow
|
||||
onDone={() => {
|
||||
void resolve()
|
||||
void resolve();
|
||||
}}
|
||||
mode="setup-token"
|
||||
startingMessage="This will guide you through long-lived (1-year) auth token setup for your Claude account. Claude subscription required."
|
||||
@@ -52,75 +48,63 @@ export async function setupTokenHandler(root: Root): Promise<void> {
|
||||
</Box>
|
||||
</KeybindingSetup>
|
||||
</AppStateProvider>,
|
||||
)
|
||||
})
|
||||
root.unmount()
|
||||
process.exit(0)
|
||||
);
|
||||
});
|
||||
root.unmount();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// DoctorWithPlugins wrapper + doctor handler
|
||||
const DoctorLazy = React.lazy(() =>
|
||||
import('../../screens/Doctor.js').then(m => ({ default: m.Doctor })),
|
||||
)
|
||||
const DoctorLazy = React.lazy(() => import('../../screens/Doctor.js').then(m => ({ default: m.Doctor })));
|
||||
|
||||
function DoctorWithPlugins({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: () => void
|
||||
}): React.ReactNode {
|
||||
useManagePlugins()
|
||||
function DoctorWithPlugins({ onDone }: { onDone: () => void }): React.ReactNode {
|
||||
useManagePlugins();
|
||||
return (
|
||||
<React.Suspense fallback={null}>
|
||||
<DoctorLazy onDone={onDone} />
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function doctorHandler(root: Root): Promise<void> {
|
||||
logEvent('tengu_doctor_command', {})
|
||||
logEvent('tengu_doctor_command', {});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
root.render(
|
||||
<AppStateProvider>
|
||||
<KeybindingSetup>
|
||||
<MCPConnectionManager
|
||||
dynamicMcpConfig={undefined}
|
||||
isStrictMcpConfig={false}
|
||||
>
|
||||
<MCPConnectionManager dynamicMcpConfig={undefined} isStrictMcpConfig={false}>
|
||||
<DoctorWithPlugins
|
||||
onDone={() => {
|
||||
void resolve()
|
||||
void resolve();
|
||||
}}
|
||||
/>
|
||||
</MCPConnectionManager>
|
||||
</KeybindingSetup>
|
||||
</AppStateProvider>,
|
||||
)
|
||||
})
|
||||
root.unmount()
|
||||
process.exit(0)
|
||||
);
|
||||
});
|
||||
root.unmount();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// install handler
|
||||
export async function installHandler(
|
||||
target: string | undefined,
|
||||
options: { force?: boolean },
|
||||
): Promise<void> {
|
||||
const { setup } = await import('../../setup.js')
|
||||
await setup(cwd(), 'default', false, false, undefined, false)
|
||||
const { install } = await import('../../commands/install.js')
|
||||
export async function installHandler(target: string | undefined, options: { force?: boolean }): Promise<void> {
|
||||
const { setup } = await import('../../setup.js');
|
||||
await setup(cwd(), 'default', false, false, undefined, false);
|
||||
const { install } = await import('../../commands/install.js');
|
||||
await new Promise<void>(resolve => {
|
||||
const args: string[] = []
|
||||
if (target) args.push(target)
|
||||
if (options.force) args.push('--force')
|
||||
const args: string[] = [];
|
||||
if (target) args.push(target);
|
||||
if (options.force) args.push('--force');
|
||||
|
||||
void install.call(
|
||||
result => {
|
||||
void resolve()
|
||||
process.exit(result.includes('failed') ? 1 : 0)
|
||||
void resolve();
|
||||
process.exit(result.includes('failed') ? 1 : 0);
|
||||
},
|
||||
{},
|
||||
args,
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type ask = any;
|
||||
export type ask = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type installOAuthTokens = any;
|
||||
export type installOAuthTokens = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type RemoteIO = any;
|
||||
export type RemoteIO = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type StructuredIO = any;
|
||||
export type StructuredIO = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type collectContextData = any;
|
||||
export type collectContextData = any
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type SDKStatus = any;
|
||||
export type ModelInfo = any;
|
||||
export type SDKMessage = any;
|
||||
export type SDKUserMessage = any;
|
||||
export type SDKUserMessageReplay = any;
|
||||
export type PermissionResult = any;
|
||||
export type McpServerConfigForProcessTransport = any;
|
||||
export type McpServerStatus = any;
|
||||
export type RewindFilesResult = any;
|
||||
export type HookEvent = any;
|
||||
export type HookInput = any;
|
||||
export type HookJSONOutput = any;
|
||||
export type PermissionUpdate = any;
|
||||
export type SDKStatus = any
|
||||
export type ModelInfo = any
|
||||
export type SDKMessage = any
|
||||
export type SDKUserMessage = any
|
||||
export type SDKUserMessageReplay = any
|
||||
export type PermissionResult = any
|
||||
export type McpServerConfigForProcessTransport = any
|
||||
export type McpServerStatus = any
|
||||
export type RewindFilesResult = any
|
||||
export type HookEvent = any
|
||||
export type HookInput = any
|
||||
export type HookJSONOutput = any
|
||||
export type PermissionUpdate = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type SDKControlElicitationResponseSchema = any;
|
||||
export type SDKControlElicitationResponseSchema = any
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type StdoutMessage = any;
|
||||
export type SDKControlInitializeRequest = any;
|
||||
export type SDKControlInitializeResponse = any;
|
||||
export type SDKControlRequest = any;
|
||||
export type SDKControlResponse = any;
|
||||
export type SDKControlMcpSetServersResponse = any;
|
||||
export type SDKControlReloadPluginsResponse = any;
|
||||
export type StdinMessage = any;
|
||||
export type StdoutMessage = any
|
||||
export type SDKControlInitializeRequest = any
|
||||
export type SDKControlInitializeResponse = any
|
||||
export type SDKControlRequest = any
|
||||
export type SDKControlResponse = any
|
||||
export type SDKControlMcpSetServersResponse = any
|
||||
export type SDKControlReloadPluginsResponse = any
|
||||
export type StdinMessage = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type CanUseToolFn = any;
|
||||
export type CanUseToolFn = any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type tryGenerateSuggestion = any;
|
||||
export type logSuggestionOutcome = any;
|
||||
export type logSuggestionSuppressed = any;
|
||||
export type PromptVariant = any;
|
||||
export type tryGenerateSuggestion = any
|
||||
export type logSuggestionOutcome = any
|
||||
export type logSuggestionSuppressed = any
|
||||
export type PromptVariant = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getFeatureValue_CACHED_MAY_BE_STALE = any;
|
||||
export type getFeatureValue_CACHED_MAY_BE_STALE = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type logEvent = any;
|
||||
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = any;
|
||||
export type logEvent = any
|
||||
export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type isQualifiedForGrove = any;
|
||||
export type checkGroveForNonInteractive = any;
|
||||
export type isQualifiedForGrove = any
|
||||
export type checkGroveForNonInteractive = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type EMPTY_USAGE = any;
|
||||
export type EMPTY_USAGE = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type statusListeners = any;
|
||||
export type ClaudeAILimits = any;
|
||||
export type statusListeners = any
|
||||
export type ClaudeAILimits = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type performMCPOAuthFlow = any;
|
||||
export type revokeServerTokens = any;
|
||||
export type performMCPOAuthFlow = any
|
||||
export type revokeServerTokens = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type isChannelAllowlisted = any;
|
||||
export type isChannelsEnabled = any;
|
||||
export type isChannelAllowlisted = any
|
||||
export type isChannelsEnabled = any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type ChannelMessageNotificationSchema = any;
|
||||
export type gateChannelServer = any;
|
||||
export type wrapChannelMessage = any;
|
||||
export type findChannelEntry = any;
|
||||
export type ChannelMessageNotificationSchema = any
|
||||
export type gateChannelServer = any
|
||||
export type wrapChannelMessage = any
|
||||
export type findChannelEntry = any
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type setupSdkMcpClients = any;
|
||||
export type connectToServer = any;
|
||||
export type clearServerCache = any;
|
||||
export type fetchToolsForClient = any;
|
||||
export type areMcpConfigsEqual = any;
|
||||
export type reconnectMcpServerImpl = any;
|
||||
export type setupSdkMcpClients = any
|
||||
export type connectToServer = any
|
||||
export type clearServerCache = any
|
||||
export type fetchToolsForClient = any
|
||||
export type areMcpConfigsEqual = any
|
||||
export type reconnectMcpServerImpl = any
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type filterMcpServersByPolicy = any;
|
||||
export type getMcpConfigByName = any;
|
||||
export type isMcpServerDisabled = any;
|
||||
export type setMcpServerEnabled = any;
|
||||
export type getAllMcpConfigs = any;
|
||||
export type filterMcpServersByPolicy = any
|
||||
export type getMcpConfigByName = any
|
||||
export type isMcpServerDisabled = any
|
||||
export type setMcpServerEnabled = any
|
||||
export type getAllMcpConfigs = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type runElicitationHooks = any;
|
||||
export type runElicitationResultHooks = any;
|
||||
export type runElicitationHooks = any
|
||||
export type runElicitationResultHooks = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getMcpPrefix = any;
|
||||
export type getMcpPrefix = any
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type MCPServerConnection = any;
|
||||
export type McpSdkServerConfig = any;
|
||||
export type ScopedMcpServerConfig = any;
|
||||
export type MCPServerConnection = any
|
||||
export type McpSdkServerConfig = any
|
||||
export type ScopedMcpServerConfig = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type commandBelongsToServer = any;
|
||||
export type filterToolsByServer = any;
|
||||
export type commandBelongsToServer = any
|
||||
export type filterToolsByServer = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type setupVscodeSdkMcp = any;
|
||||
export type setupVscodeSdkMcp = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type OAuthService = any;
|
||||
export type OAuthService = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type isPolicyAllowed = any;
|
||||
export type isPolicyAllowed = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type waitForRemoteManagedSettingsToLoad = any;
|
||||
export type waitForRemoteManagedSettingsToLoad = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type downloadUserSettings = any;
|
||||
export type redownloadUserSettings = any;
|
||||
export type downloadUserSettings = any
|
||||
export type redownloadUserSettings = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type AppState = any;
|
||||
export type AppState = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type externalMetadataToAppState = any;
|
||||
export type externalMetadataToAppState = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type assembleToolPool = any;
|
||||
export type filterToolsByDenyRules = any;
|
||||
export type assembleToolPool = any
|
||||
export type filterToolsByDenyRules = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type createAbortController = any;
|
||||
export type createAbortController = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type uniq = any;
|
||||
export type uniq = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getAccountInformation = any;
|
||||
export type getAccountInformation = any
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getLatestVersion = any;
|
||||
export type InstallStatus = any;
|
||||
export type installGlobalPackage = any;
|
||||
export type getLatestVersion = any
|
||||
export type InstallStatus = any
|
||||
export type installGlobalPackage = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type AwsAuthStatusManager = any;
|
||||
export type AwsAuthStatusManager = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type modelSupportsAutoMode = any;
|
||||
export type modelSupportsAutoMode = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type registerCleanup = any;
|
||||
export type registerCleanup = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type createCombinedAbortSignal = any;
|
||||
export type createCombinedAbortSignal = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type notifyCommandLifecycle = any;
|
||||
export type notifyCommandLifecycle = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type incrementPromptCount = any;
|
||||
export type incrementPromptCount = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type regenerateCompletionCache = any;
|
||||
export type regenerateCompletionCache = any
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getGlobalConfig = any;
|
||||
export type InstallMethod = any;
|
||||
export type saveGlobalConfig = any;
|
||||
export type getGlobalConfig = any
|
||||
export type InstallMethod = any
|
||||
export type saveGlobalConfig = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type loadConversationForResume = any;
|
||||
export type TurnInterruptionState = any;
|
||||
export type loadConversationForResume = any
|
||||
export type TurnInterruptionState = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getCwd = any;
|
||||
export type getCwd = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type logForDebugging = any;
|
||||
export type logForDebugging = any
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type logForDiagnosticsNoPII = any;
|
||||
export type withDiagnosticsTiming = any;
|
||||
export type logForDiagnosticsNoPII = any
|
||||
export type withDiagnosticsTiming = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getDoctorDiagnostic = any;
|
||||
export type getDoctorDiagnostic = any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type modelSupportsEffort = any;
|
||||
export type modelSupportsMaxEffort = any;
|
||||
export type EFFORT_LEVELS = any;
|
||||
export type resolveAppliedEffort = any;
|
||||
export type modelSupportsEffort = any
|
||||
export type modelSupportsMaxEffort = any
|
||||
export type EFFORT_LEVELS = any
|
||||
export type resolveAppliedEffort = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type AbortError = any;
|
||||
export type AbortError = any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type isFastModeAvailable = any;
|
||||
export type isFastModeEnabled = any;
|
||||
export type isFastModeSupportedByModel = any;
|
||||
export type getFastModeState = any;
|
||||
export type isFastModeAvailable = any
|
||||
export type isFastModeEnabled = any
|
||||
export type isFastModeSupportedByModel = any
|
||||
export type getFastModeState = any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type fileHistoryRewind = any;
|
||||
export type fileHistoryCanRestore = any;
|
||||
export type fileHistoryEnabled = any;
|
||||
export type fileHistoryGetDiffStats = any;
|
||||
export type fileHistoryRewind = any
|
||||
export type fileHistoryCanRestore = any
|
||||
export type fileHistoryEnabled = any
|
||||
export type fileHistoryGetDiffStats = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type executeFilePersistence = any;
|
||||
export type executeFilePersistence = any
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type createFileStateCacheWithSizeLimit = any;
|
||||
export type mergeFileStateCaches = any;
|
||||
export type READ_FILE_STATE_CACHE_SIZE = any;
|
||||
export type createFileStateCacheWithSizeLimit = any
|
||||
export type mergeFileStateCaches = any
|
||||
export type READ_FILE_STATE_CACHE_SIZE = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getLastCacheSafeParams = any;
|
||||
export type getLastCacheSafeParams = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type fromArray = any;
|
||||
export type fromArray = any
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type gracefulShutdown = any;
|
||||
export type gracefulShutdownSync = any;
|
||||
export type isShuttingDown = any;
|
||||
export type gracefulShutdown = any
|
||||
export type gracefulShutdownSync = any
|
||||
export type isShuttingDown = any
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type headlessProfilerStartTurn = any;
|
||||
export type headlessProfilerCheckpoint = any;
|
||||
export type logHeadlessProfilerTurn = any;
|
||||
export type headlessProfilerStartTurn = any
|
||||
export type headlessProfilerCheckpoint = any
|
||||
export type logHeadlessProfilerTurn = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type executeNotificationHooks = any;
|
||||
export type executeNotificationHooks = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type finalizePendingAsyncHooks = any;
|
||||
export type finalizePendingAsyncHooks = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type registerHookEventHandler = any;
|
||||
export type registerHookEventHandler = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type createIdleTimeoutManager = any;
|
||||
export type createIdleTimeoutManager = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type safeParseJSON = any;
|
||||
export type safeParseJSON = any
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user