mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -324,7 +326,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 { closeSync, mkdirSync, openSync } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
import { buildCliLaunch, spawnCli } from '../../../utils/cliLaunch.js'
|
||||
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 { buildCliLaunch, quoteCliLaunch } from '../../../utils/cliLaunch.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
|
||||
|
||||
@@ -87,8 +87,12 @@ describe('autonomy CLI handler', () => {
|
||||
})
|
||||
|
||||
test('prints individual deep status sections for panel actions', async () => {
|
||||
const pipes = await getAutonomyDeepSectionText('pipes', { rootDir: tempDir })
|
||||
const remoteControl = await getAutonomyDeepSectionText('remote-control', { rootDir: tempDir })
|
||||
const pipes = await getAutonomyDeepSectionText('pipes', {
|
||||
rootDir: tempDir,
|
||||
})
|
||||
const remoteControl = await getAutonomyDeepSectionText('remote-control', {
|
||||
rootDir: tempDir,
|
||||
})
|
||||
|
||||
expect(pipes).toContain('# Pipes')
|
||||
expect(pipes).toContain('Pipe registry:')
|
||||
@@ -116,17 +120,24 @@ describe('autonomy CLI handler', () => {
|
||||
})
|
||||
const [waitingFlow] = await listAutonomyFlows(tempDir)
|
||||
|
||||
expect(await getAutonomyFlowsText(undefined, { rootDir: tempDir })).toContain(waitingFlow!.flowId)
|
||||
expect(await getAutonomyFlowText(waitingFlow!.flowId, { rootDir: tempDir })).toContain(
|
||||
'Current step: wait',
|
||||
)
|
||||
expect(
|
||||
await getAutonomyFlowsText(undefined, { rootDir: tempDir }),
|
||||
).toContain(waitingFlow!.flowId)
|
||||
expect(
|
||||
await getAutonomyFlowText(waitingFlow!.flowId, { rootDir: tempDir }),
|
||||
).toContain('Current step: wait')
|
||||
|
||||
const resumed = await resumeAutonomyFlowText(waitingFlow!.flowId, { rootDir: tempDir, currentDir: tempDir })
|
||||
const resumed = await resumeAutonomyFlowText(waitingFlow!.flowId, {
|
||||
rootDir: tempDir,
|
||||
currentDir: tempDir,
|
||||
})
|
||||
expect(resumed).toContain('Prepared the next managed step')
|
||||
expect(resumed).toContain('Prompt:')
|
||||
expect(resumed).toContain('Wait for manual signal')
|
||||
|
||||
const cancelled = await cancelAutonomyFlowText(waitingFlow!.flowId, { rootDir: tempDir })
|
||||
const cancelled = await cancelAutonomyFlowText(waitingFlow!.flowId, {
|
||||
rootDir: tempDir,
|
||||
})
|
||||
expect(cancelled).toContain('Cancelled flow')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,9 @@ export async function getAutonomyFlowText(
|
||||
flowId: string,
|
||||
options?: { rootDir?: string },
|
||||
): Promise<string> {
|
||||
return formatAutonomyFlowDetail(await getAutonomyFlowById(flowId, options?.rootDir))
|
||||
return formatAutonomyFlowDetail(
|
||||
await getAutonomyFlowById(flowId, options?.rootDir),
|
||||
)
|
||||
}
|
||||
|
||||
export async function autonomyFlowHandler(flowId: string): Promise<void> {
|
||||
|
||||
@@ -3,201 +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) {
|
||||
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 {
|
||||
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]) => ({
|
||||
@@ -206,104 +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') {
|
||||
console.log(`${name}: ${server.url} (SSE) - ${status}`)
|
||||
console.log(`${name}: ${server.url} (SSE) - ${status}`);
|
||||
} else if (server.type === 'http') {
|
||||
console.log(`${name}: ${server.url} (HTTP) - ${status}`)
|
||||
console.log(`${name}: ${server.url} (HTTP) - ${status}`);
|
||||
} else if (server.type === 'claudeai-proxy') {
|
||||
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 : []
|
||||
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}`);
|
||||
}
|
||||
|
||||
console.log(`${name}:`)
|
||||
console.log(` Scope: ${getScopeLabel(server.scope)}`)
|
||||
console.log(`${name}:`);
|
||||
console.log(` Scope: ${getScopeLabel(server.scope)}`);
|
||||
|
||||
// Check server health
|
||||
const status = await checkMcpServerHealth(name, server)
|
||||
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') {
|
||||
console.log(` Type: sse`)
|
||||
console.log(` URL: ${server.url}`)
|
||||
console.log(` Type: sse`);
|
||||
console.log(` URL: ${server.url}`);
|
||||
if (server.headers) {
|
||||
console.log(' Headers:')
|
||||
console.log(' Headers:');
|
||||
for (const [key, value] of Object.entries(server.headers)) {
|
||||
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}`)
|
||||
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') {
|
||||
console.log(` Type: http`)
|
||||
console.log(` URL: ${server.url}`)
|
||||
console.log(` Type: http`);
|
||||
console.log(` URL: ${server.url}`);
|
||||
if (server.headers) {
|
||||
console.log(' Headers:')
|
||||
console.log(' Headers:');
|
||||
for (const [key, value] of Object.entries(server.headers)) {
|
||||
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}`)
|
||||
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') {
|
||||
console.log(` Type: stdio`)
|
||||
console.log(` Command: ${server.command}`)
|
||||
const args = Array.isArray(server.args) ? server.args : []
|
||||
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) {
|
||||
console.log(' Environment:')
|
||||
console.log(' Environment:');
|
||||
for (const [key, value] of Object.entries(server.env)) {
|
||||
console.log(` ${key}=${value}`)
|
||||
console.log(` ${key}=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
@@ -313,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 =
|
||||
@@ -328,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 &&
|
||||
@@ -347,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(
|
||||
@@ -403,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.',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,7 +267,9 @@ export class StructuredIO {
|
||||
getPendingPermissionRequests() {
|
||||
return Array.from(this.pendingRequests.values())
|
||||
.map(entry => entry.request)
|
||||
.filter(pr => (pr.request as { subtype?: string }).subtype === 'can_use_tool')
|
||||
.filter(
|
||||
pr => (pr.request as { subtype?: string }).subtype === 'can_use_tool',
|
||||
)
|
||||
}
|
||||
|
||||
setUnexpectedResponseCallback(
|
||||
@@ -285,7 +287,14 @@ export class StructuredIO {
|
||||
* callback is aborted via the signal — otherwise the callback hangs.
|
||||
*/
|
||||
injectControlResponse(response: SDKControlResponse): void {
|
||||
const responseInner = response.response as { request_id?: string; subtype?: string; error?: string; response?: unknown } | undefined
|
||||
const responseInner = response.response as
|
||||
| {
|
||||
request_id?: string
|
||||
subtype?: string
|
||||
error?: string
|
||||
response?: unknown
|
||||
}
|
||||
| undefined
|
||||
const requestId = responseInner?.request_id
|
||||
if (!requestId) return
|
||||
const request = this.pendingRequests.get(requestId as string)
|
||||
@@ -377,7 +386,12 @@ export class StructuredIO {
|
||||
if (uuid) {
|
||||
notifyCommandLifecycle(uuid, 'completed')
|
||||
}
|
||||
const resp = message.response as { request_id: string; subtype: string; response?: Record<string, unknown>; error?: string }
|
||||
const resp = message.response as {
|
||||
request_id: string
|
||||
subtype: string
|
||||
response?: Record<string, unknown>
|
||||
error?: string
|
||||
}
|
||||
const request = this.pendingRequests.get(resp.request_id)
|
||||
if (!request) {
|
||||
// Check if this tool_use was already resolved through the normal
|
||||
@@ -386,9 +400,7 @@ export class StructuredIO {
|
||||
// re-processing them would push duplicate assistant messages into
|
||||
// the conversation, causing API 400 errors.
|
||||
const responsePayload =
|
||||
resp.subtype === 'success'
|
||||
? resp.response
|
||||
: undefined
|
||||
resp.subtype === 'success' ? resp.response : undefined
|
||||
const toolUseID = responsePayload?.toolUseID
|
||||
if (
|
||||
typeof toolUseID === 'string' &&
|
||||
@@ -400,7 +412,9 @@ export class StructuredIO {
|
||||
return undefined
|
||||
}
|
||||
if (this.unexpectedResponseCallback) {
|
||||
await this.unexpectedResponseCallback(message as SDKControlResponse & { uuid?: string })
|
||||
await this.unexpectedResponseCallback(
|
||||
message as SDKControlResponse & { uuid?: string },
|
||||
)
|
||||
}
|
||||
return undefined // Ignore responses for requests we don't know about
|
||||
}
|
||||
@@ -409,7 +423,8 @@ export class StructuredIO {
|
||||
// Notify the bridge when the SDK consumer resolves a can_use_tool
|
||||
// request, so it can cancel the stale permission prompt on claude.ai.
|
||||
if (
|
||||
(request.request.request as { subtype?: string }).subtype === 'can_use_tool' &&
|
||||
(request.request.request as { subtype?: string }).subtype ===
|
||||
'can_use_tool' &&
|
||||
this.onControlRequestResolved
|
||||
) {
|
||||
this.onControlRequestResolved(resp.request_id)
|
||||
@@ -455,7 +470,9 @@ export class StructuredIO {
|
||||
if (message.type === 'assistant' || message.type === 'system') {
|
||||
return message
|
||||
}
|
||||
if ((message as { message?: { role?: string } }).message?.role !== 'user') {
|
||||
if (
|
||||
(message as { message?: { role?: string } }).message?.role !== 'user'
|
||||
) {
|
||||
exitWithMessage(
|
||||
`Error: Expected message role 'user', got '${(message as { message?: { role?: string } }).message?.role}'`,
|
||||
)
|
||||
@@ -490,7 +507,10 @@ export class StructuredIO {
|
||||
throw new Error('Request aborted')
|
||||
}
|
||||
this.outbound.enqueue(message)
|
||||
if ((request as { subtype?: string }).subtype === 'can_use_tool' && this.onControlRequestSent) {
|
||||
if (
|
||||
(request as { subtype?: string }).subtype === 'can_use_tool' &&
|
||||
this.onControlRequestSent
|
||||
) {
|
||||
this.onControlRequestSent(message)
|
||||
}
|
||||
const aborted = () => {
|
||||
@@ -820,7 +840,8 @@ async function executePermissionRequestHooksForSDK(
|
||||
const finalInput = decision.updatedInput || input
|
||||
|
||||
// Apply permission updates if provided by hook ("always allow")
|
||||
const permissionUpdates = (decision.updatedPermissions ?? []) as unknown as InternalPermissionUpdate[]
|
||||
const permissionUpdates = (decision.updatedPermissions ??
|
||||
[]) as unknown as InternalPermissionUpdate[]
|
||||
if (permissionUpdates.length > 0) {
|
||||
persistPermissionUpdates(permissionUpdates)
|
||||
const currentAppState = toolUseContext.getAppState()
|
||||
|
||||
@@ -244,7 +244,7 @@ export class HybridTransport extends WebSocketTransport {
|
||||
) {
|
||||
rcLog(
|
||||
`Hybrid POST ${response.status}: url=${this.postUrl.replace(/token=[^&]+/, 'token=***')}` +
|
||||
` events=${events.length} body=${JSON.stringify(response.data).slice(0, 200)}`,
|
||||
` events=${events.length} body=${JSON.stringify(response.data).slice(0, 200)}`,
|
||||
)
|
||||
logForDebugging(
|
||||
`HybridTransport: POST returned ${response.status} (permanent), dropping`,
|
||||
|
||||
@@ -82,9 +82,7 @@ export function parseSSEFrames(buffer: string): {
|
||||
for (const rawLine of rawFrame.split('\n')) {
|
||||
// Normalize CRLF lines in mixed-line-ending streams.
|
||||
const line =
|
||||
rawLine[rawLine.length - 1] === '\r'
|
||||
? rawLine.slice(0, -1)
|
||||
: rawLine
|
||||
rawLine[rawLine.length - 1] === '\r' ? rawLine.slice(0, -1) : rawLine
|
||||
|
||||
if (line.startsWith(':')) {
|
||||
// SSE comment (e.g., `:keepalive`)
|
||||
@@ -482,9 +480,9 @@ export class SSETransport implements Transport {
|
||||
private handleConnectionError(): void {
|
||||
rcLog(
|
||||
`SSE handleConnectionError: state=${this.state}` +
|
||||
` lastSeqNum=${this.getLastSequenceNum()}` +
|
||||
` reconnectAttempts=${this.reconnectAttempts}` +
|
||||
` msSinceLastActivity=${this.lastActivityTime > 0 ? Date.now() - this.lastActivityTime : -1}`,
|
||||
` lastSeqNum=${this.getLastSequenceNum()}` +
|
||||
` reconnectAttempts=${this.reconnectAttempts}` +
|
||||
` msSinceLastActivity=${this.lastActivityTime > 0 ? Date.now() - this.lastActivityTime : -1}`,
|
||||
)
|
||||
this.clearLivenessTimer()
|
||||
|
||||
@@ -561,8 +559,8 @@ export class SSETransport implements Transport {
|
||||
this.livenessTimer = null
|
||||
rcLog(
|
||||
`SSE liveness timeout (${LIVENESS_TIMEOUT_MS}ms)` +
|
||||
` lastSeqNum=${this.getLastSequenceNum()}` +
|
||||
` state=${this.state}`,
|
||||
` lastSeqNum=${this.getLastSequenceNum()}` +
|
||||
` state=${this.state}`,
|
||||
)
|
||||
logForDebugging('SSETransport: Liveness timeout, reconnecting', {
|
||||
level: 'error',
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export type Transport = any;
|
||||
export type Transport = any
|
||||
|
||||
@@ -398,10 +398,10 @@ export class WebSocketTransport implements Transport {
|
||||
private handleConnectionError(closeCode?: number): void {
|
||||
rcLog(
|
||||
`WS handleConnectionError: code=${closeCode}` +
|
||||
` state=${this.state}` +
|
||||
` url=${this.url.href.replace(/token=[^&]+/, 'token=***')}` +
|
||||
` msSinceLastActivity=${this.lastActivityTime > 0 ? Date.now() - this.lastActivityTime : -1}` +
|
||||
` reconnectAttempts=${this.reconnectAttempts}`,
|
||||
` state=${this.state}` +
|
||||
` url=${this.url.href.replace(/token=[^&]+/, 'token=***')}` +
|
||||
` msSinceLastActivity=${this.lastActivityTime > 0 ? Date.now() - this.lastActivityTime : -1}` +
|
||||
` reconnectAttempts=${this.reconnectAttempts}`,
|
||||
)
|
||||
logForDebugging(
|
||||
`WebSocketTransport: Disconnected from ${this.url.href}` +
|
||||
|
||||
@@ -35,7 +35,8 @@ describe('parseSSEFrames', () => {
|
||||
})
|
||||
|
||||
test('keeps incomplete trailing frame in remaining buffer for CRLF streams', () => {
|
||||
const input = 'event: client_event\r\ndata: {"ok":true}\r\n\r\ndata: {"tail":1}\r\n'
|
||||
const input =
|
||||
'event: client_event\r\ndata: {"ok":true}\r\n\r\ndata: {"tail":1}\r\n'
|
||||
const { frames, remaining } = parseSSEFrames(input)
|
||||
|
||||
expect(frames).toEqual([
|
||||
|
||||
@@ -180,7 +180,10 @@ export function accumulateStreamEvents(
|
||||
chunks.push(delta.text as string)
|
||||
const existing = touched.get(chunks)
|
||||
if (existing) {
|
||||
;(existing.event as Record<string, unknown>).delta = { type: 'text_delta', text: chunks.join('') }
|
||||
;(existing.event as Record<string, unknown>).delta = {
|
||||
type: 'text_delta',
|
||||
text: chunks.join(''),
|
||||
}
|
||||
break
|
||||
}
|
||||
const snapshot: CoalescedStreamEvent = {
|
||||
@@ -430,7 +433,10 @@ export class CCRClient {
|
||||
'delivery batch',
|
||||
)
|
||||
if (!result.ok) {
|
||||
throw new RetryableError('delivery POST failed', (result as any).retryAfterMs)
|
||||
throw new RetryableError(
|
||||
'delivery POST failed',
|
||||
(result as any).retryAfterMs,
|
||||
)
|
||||
}
|
||||
},
|
||||
baseDelayMs: 500,
|
||||
@@ -749,7 +755,14 @@ export class CCRClient {
|
||||
}
|
||||
await this.flushStreamEventBuffer()
|
||||
if (message.type === 'assistant') {
|
||||
clearStreamAccumulatorForMessage(this.streamTextAccumulator, message as { session_id: string; parent_tool_use_id: string | null; message: { id: string } })
|
||||
clearStreamAccumulatorForMessage(
|
||||
this.streamTextAccumulator,
|
||||
message as {
|
||||
session_id: string
|
||||
parent_tool_use_id: string | null
|
||||
message: { id: string }
|
||||
},
|
||||
)
|
||||
}
|
||||
await this.eventUploader.enqueue(this.toClientEvent(message))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user