mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-19 06:45:50 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
import React from 'react'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import { TOOL_SUMMARY_MAX_LENGTH } from 'src/constants/toolLimits.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { ToolProgressData } from 'src/Tool.js'
|
||||
import type { ProgressMessage } from 'src/types/message.js'
|
||||
import { formatFileSize, truncate } from 'src/utils/format.js'
|
||||
import type { Output } from './WebFetchTool.js'
|
||||
import React from 'react';
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js';
|
||||
import { TOOL_SUMMARY_MAX_LENGTH } from 'src/constants/toolLimits.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import type { ToolProgressData } from 'src/Tool.js';
|
||||
import type { ProgressMessage } from 'src/types/message.js';
|
||||
import { formatFileSize, truncate } from 'src/utils/format.js';
|
||||
import type { Output } from './WebFetchTool.js';
|
||||
|
||||
export function renderToolUseMessage(
|
||||
{ url, prompt }: Partial<{ url: string; prompt: string }>,
|
||||
{ verbose }: { theme?: string; verbose: boolean },
|
||||
): React.ReactNode {
|
||||
if (!url) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
if (verbose) {
|
||||
return `url: "${url}"${verbose && prompt ? `, prompt: "${prompt}"` : ''}`
|
||||
return `url: "${url}"${verbose && prompt ? `, prompt: "${prompt}"` : ''}`;
|
||||
}
|
||||
return url
|
||||
return url;
|
||||
}
|
||||
|
||||
export function renderToolUseProgressMessage(): React.ReactNode {
|
||||
@@ -25,7 +25,7 @@ export function renderToolUseProgressMessage(): React.ReactNode {
|
||||
<MessageResponse height={1}>
|
||||
<Text dimColor>Fetching…</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function renderToolResultMessage(
|
||||
@@ -33,7 +33,7 @@ export function renderToolResultMessage(
|
||||
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
|
||||
{ verbose }: { verbose: boolean },
|
||||
): React.ReactNode {
|
||||
const formattedSize = formatFileSize(bytes)
|
||||
const formattedSize = formatFileSize(bytes);
|
||||
if (verbose) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
@@ -46,7 +46,7 @@ export function renderToolResultMessage(
|
||||
<Text>{result}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MessageResponse height={1}>
|
||||
@@ -54,14 +54,12 @@ export function renderToolResultMessage(
|
||||
Received <Text bold>{formattedSize}</Text> ({code} {codeText})
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function getToolUseSummary(
|
||||
input: Partial<{ url: string; prompt: string }> | undefined,
|
||||
): string | null {
|
||||
export function getToolUseSummary(input: Partial<{ url: string; prompt: string }> | undefined): string | null {
|
||||
if (!input?.url) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
return truncate(input.url, TOOL_SUMMARY_MAX_LENGTH)
|
||||
return truncate(input.url, TOOL_SUMMARY_MAX_LENGTH);
|
||||
}
|
||||
|
||||
@@ -127,7 +127,9 @@ describe('WebFetch response headers', () => {
|
||||
statusText: 'OK',
|
||||
})
|
||||
|
||||
const { clearWebFetchCache, getURLMarkdownContent } = await import('../utils')
|
||||
const { clearWebFetchCache, getURLMarkdownContent } = await import(
|
||||
'../utils'
|
||||
)
|
||||
clearWebFetchCache()
|
||||
|
||||
const result = await getURLMarkdownContent(
|
||||
|
||||
@@ -1,78 +1,80 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { isPreapprovedHost } from "../preapproved";
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { isPreapprovedHost } from '../preapproved'
|
||||
|
||||
describe("isPreapprovedHost", () => {
|
||||
test("exact hostname match returns true", () => {
|
||||
expect(isPreapprovedHost("docs.python.org", "/3/")).toBe(true);
|
||||
});
|
||||
describe('isPreapprovedHost', () => {
|
||||
test('exact hostname match returns true', () => {
|
||||
expect(isPreapprovedHost('docs.python.org', '/3/')).toBe(true)
|
||||
})
|
||||
|
||||
test("developer.mozilla.org is preapproved", () => {
|
||||
expect(isPreapprovedHost("developer.mozilla.org", "/en-US/")).toBe(true);
|
||||
});
|
||||
test('developer.mozilla.org is preapproved', () => {
|
||||
expect(isPreapprovedHost('developer.mozilla.org', '/en-US/')).toBe(true)
|
||||
})
|
||||
|
||||
test("bun.sh is preapproved", () => {
|
||||
expect(isPreapprovedHost("bun.sh", "/docs")).toBe(true);
|
||||
});
|
||||
test('bun.sh is preapproved', () => {
|
||||
expect(isPreapprovedHost('bun.sh', '/docs')).toBe(true)
|
||||
})
|
||||
|
||||
test("unknown hostname returns false", () => {
|
||||
expect(isPreapprovedHost("evil.com", "/")).toBe(false);
|
||||
});
|
||||
test('unknown hostname returns false', () => {
|
||||
expect(isPreapprovedHost('evil.com', '/')).toBe(false)
|
||||
})
|
||||
|
||||
test("localhost is not preapproved", () => {
|
||||
expect(isPreapprovedHost("localhost", "/")).toBe(false);
|
||||
});
|
||||
test('localhost is not preapproved', () => {
|
||||
expect(isPreapprovedHost('localhost', '/')).toBe(false)
|
||||
})
|
||||
|
||||
test("empty hostname returns false", () => {
|
||||
expect(isPreapprovedHost("", "/")).toBe(false);
|
||||
});
|
||||
test('empty hostname returns false', () => {
|
||||
expect(isPreapprovedHost('', '/')).toBe(false)
|
||||
})
|
||||
|
||||
test("path-scoped entry matches exact path", () => {
|
||||
test('path-scoped entry matches exact path', () => {
|
||||
// github.com/anthropics is a path-scoped entry
|
||||
expect(isPreapprovedHost("github.com", "/anthropics")).toBe(true);
|
||||
});
|
||||
expect(isPreapprovedHost('github.com', '/anthropics')).toBe(true)
|
||||
})
|
||||
|
||||
test("path-scoped entry matches sub-path", () => {
|
||||
expect(isPreapprovedHost("github.com", "/anthropics/claude-code")).toBe(true);
|
||||
});
|
||||
test('path-scoped entry matches sub-path', () => {
|
||||
expect(isPreapprovedHost('github.com', '/anthropics/claude-code')).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test("path-scoped entry does not match other paths", () => {
|
||||
test('path-scoped entry does not match other paths', () => {
|
||||
// github.com is NOT in the hostname-only set (only github.com/anthropics is)
|
||||
expect(isPreapprovedHost("github.com", "/torvalds/linux")).toBe(false);
|
||||
});
|
||||
expect(isPreapprovedHost('github.com', '/torvalds/linux')).toBe(false)
|
||||
})
|
||||
|
||||
test("path-scoped entry with trailing slash", () => {
|
||||
expect(isPreapprovedHost("github.com", "/anthropics/")).toBe(true);
|
||||
});
|
||||
test('path-scoped entry with trailing slash', () => {
|
||||
expect(isPreapprovedHost('github.com', '/anthropics/')).toBe(true)
|
||||
})
|
||||
|
||||
test("vercel.com/docs matches (path-scoped)", () => {
|
||||
expect(isPreapprovedHost("vercel.com", "/docs")).toBe(true);
|
||||
});
|
||||
test('vercel.com/docs matches (path-scoped)', () => {
|
||||
expect(isPreapprovedHost('vercel.com', '/docs')).toBe(true)
|
||||
})
|
||||
|
||||
test("vercel.com/docs/something matches", () => {
|
||||
expect(isPreapprovedHost("vercel.com", "/docs/something")).toBe(true);
|
||||
});
|
||||
test('vercel.com/docs/something matches', () => {
|
||||
expect(isPreapprovedHost('vercel.com', '/docs/something')).toBe(true)
|
||||
})
|
||||
|
||||
test("vercel.com root does not match", () => {
|
||||
expect(isPreapprovedHost("vercel.com", "/")).toBe(false);
|
||||
});
|
||||
test('vercel.com root does not match', () => {
|
||||
expect(isPreapprovedHost('vercel.com', '/')).toBe(false)
|
||||
})
|
||||
|
||||
test("docs.netlify.com matches (path-scoped)", () => {
|
||||
expect(isPreapprovedHost("docs.netlify.com", "/")).toBe(true);
|
||||
});
|
||||
test('docs.netlify.com matches (path-scoped)', () => {
|
||||
expect(isPreapprovedHost('docs.netlify.com', '/')).toBe(true)
|
||||
})
|
||||
|
||||
test("case sensitivity — hostname must match exactly", () => {
|
||||
expect(isPreapprovedHost("Docs.Python.org", "/3/")).toBe(false);
|
||||
});
|
||||
test('case sensitivity — hostname must match exactly', () => {
|
||||
expect(isPreapprovedHost('Docs.Python.org', '/3/')).toBe(false)
|
||||
})
|
||||
|
||||
test("subdomain of preapproved host does not match", () => {
|
||||
expect(isPreapprovedHost("sub.docs.python.org", "/3/")).toBe(false);
|
||||
});
|
||||
test('subdomain of preapproved host does not match', () => {
|
||||
expect(isPreapprovedHost('sub.docs.python.org', '/3/')).toBe(false)
|
||||
})
|
||||
|
||||
test("www.typescriptlang.org is preapproved", () => {
|
||||
expect(isPreapprovedHost("www.typescriptlang.org", "/docs/")).toBe(true);
|
||||
});
|
||||
test('www.typescriptlang.org is preapproved', () => {
|
||||
expect(isPreapprovedHost('www.typescriptlang.org', '/docs/')).toBe(true)
|
||||
})
|
||||
|
||||
test("modelcontextprotocol.io is preapproved", () => {
|
||||
expect(isPreapprovedHost("modelcontextprotocol.io", "/")).toBe(true);
|
||||
});
|
||||
});
|
||||
test('modelcontextprotocol.io is preapproved', () => {
|
||||
expect(isPreapprovedHost('modelcontextprotocol.io', '/')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
// Re-implement the pure functions locally to avoid the heavy import chain.
|
||||
// The source implementations are in ../utils.ts — these are verified to match.
|
||||
|
||||
const MAX_URL_LENGTH = 2000;
|
||||
const MAX_URL_LENGTH = 2000
|
||||
|
||||
function validateURL(url: string): boolean {
|
||||
if (url.length > MAX_URL_LENGTH) return false;
|
||||
let parsed;
|
||||
if (url.length > MAX_URL_LENGTH) return false
|
||||
let parsed
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
parsed = new URL(url)
|
||||
} catch {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (parsed.username || parsed.password) return false;
|
||||
const parts = parsed.hostname.split(".");
|
||||
if (parts.length < 2) return false;
|
||||
return true;
|
||||
if (parsed.username || parsed.password) return false
|
||||
const parts = parsed.hostname.split('.')
|
||||
if (parts.length < 2) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function isPermittedRedirect(
|
||||
@@ -24,126 +24,126 @@ function isPermittedRedirect(
|
||||
redirectUrl: string,
|
||||
): boolean {
|
||||
try {
|
||||
const parsedOriginal = new URL(originalUrl);
|
||||
const parsedRedirect = new URL(redirectUrl);
|
||||
if (parsedRedirect.protocol !== parsedOriginal.protocol) return false;
|
||||
if (parsedRedirect.port !== parsedOriginal.port) return false;
|
||||
if (parsedRedirect.username || parsedRedirect.password) return false;
|
||||
const stripWww = (hostname: string) => hostname.replace(/^www\./, "");
|
||||
const parsedOriginal = new URL(originalUrl)
|
||||
const parsedRedirect = new URL(redirectUrl)
|
||||
if (parsedRedirect.protocol !== parsedOriginal.protocol) return false
|
||||
if (parsedRedirect.port !== parsedOriginal.port) return false
|
||||
if (parsedRedirect.username || parsedRedirect.password) return false
|
||||
const stripWww = (hostname: string) => hostname.replace(/^www\./, '')
|
||||
return (
|
||||
stripWww(parsedOriginal.hostname) === stripWww(parsedRedirect.hostname)
|
||||
);
|
||||
)
|
||||
} catch {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
describe("validateURL", () => {
|
||||
test("accepts valid https URL", () => {
|
||||
expect(validateURL("https://example.com/path")).toBe(true);
|
||||
});
|
||||
describe('validateURL', () => {
|
||||
test('accepts valid https URL', () => {
|
||||
expect(validateURL('https://example.com/path')).toBe(true)
|
||||
})
|
||||
|
||||
test("accepts valid http URL", () => {
|
||||
expect(validateURL("http://example.com/path")).toBe(true);
|
||||
});
|
||||
test('accepts valid http URL', () => {
|
||||
expect(validateURL('http://example.com/path')).toBe(true)
|
||||
})
|
||||
|
||||
test("rejects URL without protocol", () => {
|
||||
expect(validateURL("example.com")).toBe(false);
|
||||
});
|
||||
test('rejects URL without protocol', () => {
|
||||
expect(validateURL('example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects URL with username", () => {
|
||||
expect(validateURL("https://user@example.com/path")).toBe(false);
|
||||
});
|
||||
test('rejects URL with username', () => {
|
||||
expect(validateURL('https://user@example.com/path')).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects URL with password", () => {
|
||||
expect(validateURL("https://user:pass@example.com/path")).toBe(false);
|
||||
});
|
||||
test('rejects URL with password', () => {
|
||||
expect(validateURL('https://user:pass@example.com/path')).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects single-label hostname", () => {
|
||||
expect(validateURL("https://localhost/path")).toBe(false);
|
||||
});
|
||||
test('rejects single-label hostname', () => {
|
||||
expect(validateURL('https://localhost/path')).toBe(false)
|
||||
})
|
||||
|
||||
test("accepts URL with query params", () => {
|
||||
expect(validateURL("https://example.com/path?q=test")).toBe(true);
|
||||
});
|
||||
test('accepts URL with query params', () => {
|
||||
expect(validateURL('https://example.com/path?q=test')).toBe(true)
|
||||
})
|
||||
|
||||
test("accepts URL with port", () => {
|
||||
expect(validateURL("https://example.com:8080/path")).toBe(true);
|
||||
});
|
||||
test('accepts URL with port', () => {
|
||||
expect(validateURL('https://example.com:8080/path')).toBe(true)
|
||||
})
|
||||
|
||||
test("rejects empty string", () => {
|
||||
expect(validateURL("")).toBe(false);
|
||||
});
|
||||
test('rejects empty string', () => {
|
||||
expect(validateURL('')).toBe(false)
|
||||
})
|
||||
|
||||
test("accepts URL with subdomain", () => {
|
||||
expect(validateURL("https://docs.example.com/path")).toBe(true);
|
||||
});
|
||||
test('accepts URL with subdomain', () => {
|
||||
expect(validateURL('https://docs.example.com/path')).toBe(true)
|
||||
})
|
||||
|
||||
test("rejects very long URL", () => {
|
||||
const longUrl = "https://example.com/" + "a".repeat(MAX_URL_LENGTH);
|
||||
expect(validateURL(longUrl)).toBe(false);
|
||||
});
|
||||
});
|
||||
test('rejects very long URL', () => {
|
||||
const longUrl = 'https://example.com/' + 'a'.repeat(MAX_URL_LENGTH)
|
||||
expect(validateURL(longUrl)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isPermittedRedirect", () => {
|
||||
test("same host different path is permitted", () => {
|
||||
describe('isPermittedRedirect', () => {
|
||||
test('same host different path is permitted', () => {
|
||||
expect(
|
||||
isPermittedRedirect("https://example.com/old", "https://example.com/new"),
|
||||
).toBe(true);
|
||||
});
|
||||
isPermittedRedirect('https://example.com/old', 'https://example.com/new'),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("adding www is permitted", () => {
|
||||
test('adding www is permitted', () => {
|
||||
expect(
|
||||
isPermittedRedirect(
|
||||
"https://example.com/path",
|
||||
"https://www.example.com/path",
|
||||
'https://example.com/path',
|
||||
'https://www.example.com/path',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("removing www is permitted", () => {
|
||||
test('removing www is permitted', () => {
|
||||
expect(
|
||||
isPermittedRedirect(
|
||||
"https://www.example.com/path",
|
||||
"https://example.com/path",
|
||||
'https://www.example.com/path',
|
||||
'https://example.com/path',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("different host is not permitted", () => {
|
||||
test('different host is not permitted', () => {
|
||||
expect(
|
||||
isPermittedRedirect("https://example.com/path", "https://other.com/path"),
|
||||
).toBe(false);
|
||||
});
|
||||
isPermittedRedirect('https://example.com/path', 'https://other.com/path'),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test("protocol change is not permitted", () => {
|
||||
test('protocol change is not permitted', () => {
|
||||
expect(
|
||||
isPermittedRedirect(
|
||||
"https://example.com/path",
|
||||
"http://example.com/path",
|
||||
'https://example.com/path',
|
||||
'http://example.com/path',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
test("invalid URL returns false", () => {
|
||||
expect(isPermittedRedirect("not-a-url", "also-not-a-url")).toBe(false);
|
||||
});
|
||||
test('invalid URL returns false', () => {
|
||||
expect(isPermittedRedirect('not-a-url', 'also-not-a-url')).toBe(false)
|
||||
})
|
||||
|
||||
test("same URL is permitted", () => {
|
||||
test('same URL is permitted', () => {
|
||||
expect(
|
||||
isPermittedRedirect(
|
||||
"https://example.com/path",
|
||||
"https://example.com/path",
|
||||
'https://example.com/path',
|
||||
'https://example.com/path',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("redirect with credentials is not permitted", () => {
|
||||
test('redirect with credentials is not permitted', () => {
|
||||
expect(
|
||||
isPermittedRedirect(
|
||||
"https://example.com/path",
|
||||
"https://user@example.com/path",
|
||||
'https://example.com/path',
|
||||
'https://user@example.com/path',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -314,7 +314,10 @@ export async function getWithPermittedRedirects(
|
||||
error.response &&
|
||||
[301, 302, 307, 308].includes(error.response.status)
|
||||
) {
|
||||
const redirectLocation = getResponseHeader(error.response.headers, 'location')
|
||||
const redirectLocation = getResponseHeader(
|
||||
error.response.headers,
|
||||
'location',
|
||||
)
|
||||
if (!redirectLocation) {
|
||||
throw new Error('Redirect missing Location header')
|
||||
}
|
||||
@@ -551,7 +554,11 @@ export async function applyPromptToMarkdown(
|
||||
const { content } = assistantMessage.message!
|
||||
if (content!.length > 0) {
|
||||
const contentBlock = content![0]
|
||||
if (contentBlock && typeof contentBlock === 'object' && 'text' in contentBlock) {
|
||||
if (
|
||||
contentBlock &&
|
||||
typeof contentBlock === 'object' &&
|
||||
'text' in contentBlock
|
||||
) {
|
||||
return (contentBlock as { text: string }).text
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user