mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e61e71c54 | ||
|
|
b8b48bf7ed | ||
|
|
de9dbcdcbb | ||
|
|
0a9e6c0313 | ||
|
|
73130bded3 | ||
|
|
1a1d57057e | ||
|
|
7f864a4743 | ||
|
|
c81dac8c3c | ||
|
|
4266149820 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "1.10.5",
|
"version": "1.10.9",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ export const DEFAULT_BUILD_FEATURES = [
|
|||||||
'CONTEXT_COLLAPSE', // 上下文折叠,自动压缩旧消息
|
'CONTEXT_COLLAPSE', // 上下文折叠,自动压缩旧消息
|
||||||
'MONITOR_TOOL', // Monitor 工具,流式监控后台进程输出
|
'MONITOR_TOOL', // Monitor 工具,流式监控后台进程输出
|
||||||
'FORK_SUBAGENT', // Fork 子代理,在隔离上下文中并行执行任务
|
'FORK_SUBAGENT', // Fork 子代理,在隔离上下文中并行执行任务
|
||||||
'UDS_INBOX', // inbox 数组只增不减(非 GB 级主因)
|
// 'UDS_INBOX', // inbox 数组只增不减(非 GB 级主因)
|
||||||
'KAIROS', // Kairos 定时任务系统核心
|
'KAIROS', // Kairos 定时任务系统核心
|
||||||
// 'COORDINATOR_MODE', // 已禁用:AgentSummary 30s fork 循环,GB 级泄露主因
|
// 'COORDINATOR_MODE', // 已禁用:AgentSummary 30s fork 循环,GB 级泄露主因
|
||||||
'LAN_PIPES', // 依赖 UDS_INBOX(已随 UDS_INBOX 恢复)
|
// 'LAN_PIPES', // 依赖 UDS_INBOX(已随 UDS_INBOX 恢复)
|
||||||
'BG_SESSIONS', // 后台会话管理(ps/logs/attach/kill)
|
'BG_SESSIONS', // 后台会话管理(ps/logs/attach/kill)
|
||||||
'TEMPLATES', // 模板任务(new/list/reply 子命令)
|
'TEMPLATES', // 模板任务(new/list/reply 子命令)
|
||||||
// 'REVIEW_ARTIFACT', // 代码审查产物(API 请求无响应,待排查 schema 兼容性)
|
// 'REVIEW_ARTIFACT', // 代码审查产物(API 请求无响应,待排查 schema 兼容性)
|
||||||
@@ -68,7 +68,7 @@ export const DEFAULT_BUILD_FEATURES = [
|
|||||||
'DIRECT_CONNECT', // 直连模式(claude server / claude open)
|
'DIRECT_CONNECT', // 直连模式(claude server / claude open)
|
||||||
// Skill search & learning
|
// Skill search & learning
|
||||||
'EXPERIMENTAL_SKILL_SEARCH', // 实验性技能搜索(DiscoverSkills)
|
'EXPERIMENTAL_SKILL_SEARCH', // 实验性技能搜索(DiscoverSkills)
|
||||||
'SKILL_LEARNING', // projectContext cache 无淘汰机制(非 GB 级主因)
|
// 'SKILL_LEARNING', // projectContext cache 无淘汰机制(非 GB 级主因)
|
||||||
// P3: poor mode
|
// P3: poor mode
|
||||||
'POOR', // 穷鬼模式,跳过 extract_memories/prompt_suggestion 减少消耗
|
'POOR', // 穷鬼模式,跳过 extract_memories/prompt_suggestion 减少消耗
|
||||||
// Team Memory
|
// Team Memory
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ describe('startAgentSummarization', () => {
|
|||||||
lastTimerHandle = undefined
|
lastTimerHandle = undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function expectDebugLogContaining(fragment: string): void {
|
||||||
|
expect(debugLogs.some(message => message.includes(fragment))).toBe(true)
|
||||||
|
}
|
||||||
|
|
||||||
test('summarizes bounded transcript once and skips unchanged fingerprints', async () => {
|
test('summarizes bounded transcript once and skips unchanged fingerprints', async () => {
|
||||||
handle = startTestSummarization()
|
handle = startTestSummarization()
|
||||||
|
|
||||||
@@ -157,7 +161,7 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expect(debugLogs).toContain(
|
expectDebugLogContaining(
|
||||||
'[AgentSummary] Skipping summary for task-1: no bounded context available',
|
'[AgentSummary] Skipping summary for task-1: no bounded context available',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -171,7 +175,7 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expect(debugLogs).toContain(
|
expectDebugLogContaining(
|
||||||
'[AgentSummary] Skipping summary for task-1: not enough messages (2)',
|
'[AgentSummary] Skipping summary for task-1: not enough messages (2)',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -188,9 +192,7 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expect(debugLogs).toContain(
|
expectDebugLogContaining('[AgentSummary] Skipping summary — poor mode active')
|
||||||
'[AgentSummary] Skipping summary — poor mode active',
|
|
||||||
)
|
|
||||||
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
||||||
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
||||||
})
|
})
|
||||||
@@ -220,9 +222,7 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
handle.stop()
|
handle.stop()
|
||||||
|
|
||||||
expect(debugLogs).toContain(
|
expectDebugLogContaining('[AgentSummary] Stopping summarization for task-1')
|
||||||
'[AgentSummary] Stopping summarization for task-1',
|
|
||||||
)
|
|
||||||
expect(clearedHandles).toEqual([pendingHandle])
|
expect(clearedHandles).toEqual([pendingHandle])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ function buildAgentContent(params: {
|
|||||||
'',
|
'',
|
||||||
instincts
|
instincts
|
||||||
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
||||||
|
.slice(0, 20)
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
'',
|
'',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|||||||
@@ -35,15 +35,18 @@ export function createInstinct(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_EVIDENCE_ENTRIES = 10
|
||||||
|
|
||||||
export function normalizeInstinct(instinct: StoredInstinct): StoredInstinct {
|
export function normalizeInstinct(instinct: StoredInstinct): StoredInstinct {
|
||||||
|
const uniqueEvidence = Array.from(new Set(instinct.evidence.filter(Boolean)))
|
||||||
return {
|
return {
|
||||||
...instinct,
|
...instinct,
|
||||||
id: instinct.id || buildInstinctId(instinct.trigger, instinct.action),
|
id: instinct.id || buildInstinctId(instinct.trigger, instinct.action),
|
||||||
confidence: clampConfidence(instinct.confidence),
|
confidence: clampConfidence(instinct.confidence),
|
||||||
evidence: Array.from(new Set(instinct.evidence.filter(Boolean))),
|
evidence: uniqueEvidence.slice(-MAX_EVIDENCE_ENTRIES),
|
||||||
evidenceOutcome: instinct.evidenceOutcome,
|
evidenceOutcome: instinct.evidenceOutcome,
|
||||||
observationIds: instinct.observationIds
|
observationIds: instinct.observationIds
|
||||||
? Array.from(new Set(instinct.observationIds))
|
? Array.from(new Set(instinct.observationIds)).slice(-20)
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
import type { LearnedSkillDraft, SkillLearningScope } from './types.js'
|
import type { LearnedSkillDraft, SkillLearningScope } from './types.js'
|
||||||
|
|
||||||
export const DUPLICATE_SKILL_OVERLAP_THRESHOLD = 0.8
|
export const DUPLICATE_SKILL_OVERLAP_THRESHOLD = 0.8
|
||||||
|
const MAX_EVIDENCE_LINES_PER_APPEND = 20
|
||||||
|
const MAX_EVIDENCE_LINES_IN_SKILL = 20
|
||||||
|
const MAX_SKILL_FILE_BYTES = 50_000
|
||||||
|
|
||||||
export type SkillGeneratorOptions = {
|
export type SkillGeneratorOptions = {
|
||||||
cwd?: string
|
cwd?: string
|
||||||
@@ -101,20 +104,41 @@ export async function appendInstinctEvidenceToSkill(
|
|||||||
const existing = await readFile(target.path, 'utf8').catch(
|
const existing = await readFile(target.path, 'utf8').catch(
|
||||||
() => target.content,
|
() => target.content,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Skip if the file already exceeds the size cap
|
||||||
|
if (Buffer.byteLength(existing, 'utf8') >= MAX_SKILL_FILE_BYTES) {
|
||||||
|
return target.path
|
||||||
|
}
|
||||||
|
|
||||||
|
const allEvidence = instincts.flatMap(instinct =>
|
||||||
|
instinct.evidence.map(evidence => `- ${evidence}`),
|
||||||
|
)
|
||||||
|
const evidenceLines = allEvidence.slice(0, MAX_EVIDENCE_LINES_PER_APPEND)
|
||||||
|
if (evidenceLines.length < allEvidence.length) {
|
||||||
|
evidenceLines.push(
|
||||||
|
`- [... ${allEvidence.length - evidenceLines.length} more evidence entries omitted]`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const block = [
|
const block = [
|
||||||
'',
|
'',
|
||||||
`## Learned evidence (${now})`,
|
`## Learned evidence (${now})`,
|
||||||
'',
|
'',
|
||||||
...instincts.flatMap(instinct =>
|
...evidenceLines,
|
||||||
instinct.evidence.map(evidence => `- ${evidence}`),
|
|
||||||
),
|
|
||||||
'',
|
'',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const merged = existing.endsWith('\n')
|
const merged = existing.endsWith('\n')
|
||||||
? existing + block
|
? existing + block
|
||||||
: `${existing}\n${block}`
|
: `${existing}\n${block}`
|
||||||
await writeFile(target.path, merged, 'utf8')
|
|
||||||
|
// Final guard: truncate if merged exceeds size cap
|
||||||
|
const finalContent =
|
||||||
|
Buffer.byteLength(merged, 'utf8') > MAX_SKILL_FILE_BYTES
|
||||||
|
? merged.slice(0, MAX_SKILL_FILE_BYTES)
|
||||||
|
: merged
|
||||||
|
|
||||||
|
await writeFile(target.path, finalContent, 'utf8')
|
||||||
clearSkillIndexCache()
|
clearSkillIndexCache()
|
||||||
return target.path
|
return target.path
|
||||||
}
|
}
|
||||||
@@ -191,6 +215,7 @@ function buildSkillContent(params: {
|
|||||||
'',
|
'',
|
||||||
instincts
|
instincts
|
||||||
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
||||||
|
.slice(0, MAX_EVIDENCE_LINES_IN_SKILL)
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
'',
|
'',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -365,7 +365,11 @@ describe('teammate mailbox retention', () => {
|
|||||||
if (code === undefined) {
|
if (code === undefined) {
|
||||||
throw new Error('Expected filesystem errno code')
|
throw new Error('Expected filesystem errno code')
|
||||||
}
|
}
|
||||||
expect(['EISDIR', 'EPERM', 'EACCES']).toContain(code)
|
const expectedCodes =
|
||||||
|
process.platform === 'win32'
|
||||||
|
? ['EISDIR', 'EPERM', 'EACCES']
|
||||||
|
: ['EISDIR']
|
||||||
|
expect(expectedCodes).toContain(code)
|
||||||
expect((await stat(inboxPath)).isDirectory()).toBe(true)
|
expect((await stat(inboxPath)).isDirectory()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ describe('UDS inbox retention', () => {
|
|||||||
'../udsClient.js'
|
'../udsClient.js'
|
||||||
)
|
)
|
||||||
|
|
||||||
const error = await sendToUdsSocket(path, 'hello', 50).then(
|
const error = await sendToUdsSocket(path, 'hello', 200).then(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
err => err,
|
err => err,
|
||||||
)
|
)
|
||||||
@@ -301,6 +301,75 @@ describe('UDS inbox retention', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('connectToPeer reports connection failures as peer connection errors', async () => {
|
||||||
|
const path = socketPath('uds-connect-error')
|
||||||
|
const { connectToPeer, UdsPeerConnectionError } = await import(
|
||||||
|
'../udsClient.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
const error = await connectToPeer(path, () => {
|
||||||
|
throw new Error('Unexpected post-connect socket error')
|
||||||
|
}).then(
|
||||||
|
() => undefined,
|
||||||
|
err => err,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(error).toBeInstanceOf(UdsPeerConnectionError)
|
||||||
|
if (!(error instanceof UdsPeerConnectionError)) {
|
||||||
|
throw new Error('Expected UDS peer connection error')
|
||||||
|
}
|
||||||
|
expect(error.socketPath).toBe(path)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('connectToPeer leaves connected socket lifecycle to the caller', async () => {
|
||||||
|
const path = socketPath('uds-connect-lifecycle')
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
await mkdir(dirname(path), { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const sockets = new Set<Socket>()
|
||||||
|
const receiver = createServer(socket => {
|
||||||
|
sockets.add(socket)
|
||||||
|
socket.on('close', () => {
|
||||||
|
sockets.delete(socket)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
receiver.on('error', reject)
|
||||||
|
receiver.listen(path, () => resolve())
|
||||||
|
})
|
||||||
|
|
||||||
|
let client: Socket | undefined
|
||||||
|
const socketErrors: Error[] = []
|
||||||
|
try {
|
||||||
|
const { connectToPeer } = await import('../udsClient.js')
|
||||||
|
client = await connectToPeer(
|
||||||
|
path,
|
||||||
|
error => {
|
||||||
|
socketErrors.push(error)
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(client.destroyed).toBe(false)
|
||||||
|
expect(client.listenerCount('error')).toBe(1)
|
||||||
|
|
||||||
|
const socketError = new Error('post-connect failure')
|
||||||
|
client.emit('error', socketError)
|
||||||
|
expect(socketErrors).toEqual([socketError])
|
||||||
|
} finally {
|
||||||
|
client?.destroy()
|
||||||
|
for (const socket of sockets) {
|
||||||
|
socket.destroy()
|
||||||
|
}
|
||||||
|
await closeServer(receiver)
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
await unlink(path).catch(() => undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('sendUdsMessage fails closed before connecting without an auth token', async () => {
|
test('sendUdsMessage fails closed before connecting without an auth token', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
sendUdsMessage(socketPath('no-auth-token'), { type: 'text', data: 'x' }),
|
sendUdsMessage(socketPath('no-auth-token'), { type: 'text', data: 'x' }),
|
||||||
|
|||||||
@@ -132,10 +132,11 @@ export function truncateToWidthNoEllipsis(
|
|||||||
* @returns The truncated string with ellipsis if needed
|
* @returns The truncated string with ellipsis if needed
|
||||||
*/
|
*/
|
||||||
export function truncate(
|
export function truncate(
|
||||||
str: string,
|
str: string | undefined | null,
|
||||||
maxWidth: number,
|
maxWidth: number,
|
||||||
singleLine: boolean = false,
|
singleLine: boolean = false,
|
||||||
): string {
|
): string {
|
||||||
|
if (str == null) return ''
|
||||||
let result = str
|
let result = str
|
||||||
|
|
||||||
// If singleLine is true, truncate at first newline
|
// If singleLine is true, truncate at first newline
|
||||||
|
|||||||
@@ -266,17 +266,48 @@ export async function sendToUdsSocket(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a peer and return the raw socket for bidirectional communication.
|
* Connect to a peer and return the raw socket for bidirectional communication.
|
||||||
* The caller is responsible for managing the connection lifecycle.
|
* The caller owns the post-connect lifecycle through onSocketError, which is
|
||||||
|
* attached before the Promise resolves so peer socket errors cannot be
|
||||||
|
* swallowed or surface through a listener handoff window.
|
||||||
|
* Pre-connect failures reject with UdsPeerConnectionError.
|
||||||
|
* This only opens the transport; callers still own any capability handshake.
|
||||||
*/
|
*/
|
||||||
export function connectToPeer(socketPath: string): Promise<Socket> {
|
export function connectToPeer(
|
||||||
|
socketPath: string,
|
||||||
|
onSocketError: (error: Error) => void,
|
||||||
|
timeoutMs = 5000,
|
||||||
|
): Promise<Socket> {
|
||||||
return new Promise<Socket>((resolve, reject) => {
|
return new Promise<Socket>((resolve, reject) => {
|
||||||
const conn = createConnection(socketPath, () => {
|
const conn = createConnection(socketPath)
|
||||||
|
let settled = false
|
||||||
|
const timeout = setTimeout(
|
||||||
|
fail,
|
||||||
|
timeoutMs,
|
||||||
|
new Error('Connection timed out'),
|
||||||
|
)
|
||||||
|
function cleanupListeners(): void {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
conn.off('error', fail)
|
||||||
|
}
|
||||||
|
function fail(cause: unknown): void {
|
||||||
|
if (settled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settled = true
|
||||||
|
cleanupListeners()
|
||||||
|
conn.destroy()
|
||||||
|
reject(new UdsPeerConnectionError(socketPath, cause))
|
||||||
|
}
|
||||||
|
conn.once('connect', () => {
|
||||||
|
if (settled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settled = true
|
||||||
|
cleanupListeners()
|
||||||
|
conn.on('error', onSocketError)
|
||||||
resolve(conn)
|
resolve(conn)
|
||||||
})
|
})
|
||||||
conn.on('error', reject)
|
conn.on('error', fail)
|
||||||
conn.setTimeout(5000, () => {
|
|
||||||
conn.destroy(new Error('Connection timed out'))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -557,7 +557,26 @@ export async function startUdsMessaging(
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
await chmod(path, 0o600)
|
// Restrict socket permissions to owner-only. On macOS with
|
||||||
|
// Node.js v22, the listen callback may fire before the socket
|
||||||
|
// file is visible on disk (observed with nested tmpdir paths).
|
||||||
|
// The parent directory is already 0o700, so skipping chmod when
|
||||||
|
// the file is not yet visible is safe.
|
||||||
|
try {
|
||||||
|
await chmod(path, 0o600)
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
err instanceof Error &&
|
||||||
|
(err as NodeJS.ErrnoException).code === 'ENOENT'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
logForDebugging(
|
||||||
|
`[udsMessaging] chmod skipped: socket file not yet visible at ${path}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
srv.off('error', rejectBeforeListen)
|
srv.off('error', rejectBeforeListen)
|
||||||
srv.on('error', logRuntimeError)
|
srv.on('error', logRuntimeError)
|
||||||
|
|||||||
Reference in New Issue
Block a user