Feat/integrate lint preview (#285)

* feat: 适配 zed acp 协议

* docs: 完善 acp 文档

* feat: integrate feature branches + daemon/job 命令层级化 + 跨平台后台引擎

Cherry-picked from origin/lint/preview (637c908), excluding lint-only changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct detectMimeFromBase64 to decode raw bytes from base64

Cherry-picked from origin/lint/preview (ee36954).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构

Cherry-picked from origin/lint/preview (c5f52cd), excluding lint-only formatting changes.

- 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层
- 修复 --daemon-worker=kind 等号格式解析
- 修复 daemon/bg fast path 缺少 setShellIfWindows()
- 修复 checkPathExists 用 existsSync 替代 execSync('dir')
- 7 个 spawn 站点迁移到 CliLaunchSpec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-16 20:59:29 +08:00
committed by GitHub
parent a02dc0bded
commit c8d08d235b
137 changed files with 13267 additions and 837 deletions

View File

@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -37,16 +37,21 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { randomUUID } from "node:crypto";
/** Detect actual image MIME type from base64 data using magic bytes. */
/** Detect actual image MIME type from base64 data by decoding the magic bytes. */
function detectMimeFromBase64(b64: string): string {
// First byte is enough to distinguish PNG (0x89) from JPEG (0xFF)
const c = b64.charCodeAt(0);
if (c === 0x89) return "image/png";
if (c === 0xFF) return "image/jpeg";
// RIFF = WebP
if (c === 0x52) return "image/webp";
// GIF
if (c === 0x47) return "image/gif";
// Decode first 12 raw bytes (16 base64 chars is enough) and check standard magic bytes.
// PNG: 89 50 4E 47
// JPEG: FF D8 FF
// RIFF+WEBP: "RIFF" at 0..3 + "WEBP" at 8..11
// GIF: "GIF" at 0..2
const raw = Buffer.from(b64.slice(0, 16), "base64");
if (raw[0] === 0x89 && raw[1] === 0x50 && raw[2] === 0x4e && raw[3] === 0x47) return "image/png";
if (raw[0] === 0xff && raw[1] === 0xd8 && raw[2] === 0xff) return "image/jpeg";
if (
raw[0] === 0x52 && raw[1] === 0x49 && raw[2] === 0x46 && raw[3] === 0x46 && // RIFF
raw[8] === 0x57 && raw[9] === 0x45 && raw[10] === 0x42 && raw[11] === 0x50 // WEBP
) return "image/webp";
if (raw[0] === 0x47 && raw[1] === 0x49 && raw[2] === 0x46) return "image/gif";
return "image/png";
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.json",
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,7 +1,9 @@
import { feature } from 'bun:bundle'
import { z } from 'zod/v4'
import type { ToolResultBlockParam } from 'src/Tool.js'
import { buildTool } from 'src/Tool.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import { logForDebugging } from 'src/utils/debug.js'
const PUSH_NOTIFICATION_TOOL_NAME = 'PushNotification'
@@ -74,14 +76,58 @@ Requires Remote Control to be configured. Respects user notification settings (t
}
},
async call(_input: PushInput) {
// Push delivery is handled by the Remote Control / KAIROS transport layer.
// Without the KAIROS runtime, this tool is not available.
return {
data: {
sent: false,
error: 'PushNotification requires the KAIROS transport layer.',
},
async call(input: PushInput, context) {
const appState = context.getAppState()
// Try bridge delivery first (for remote/mobile viewers)
if (appState.replBridgeEnabled) {
if (feature('BRIDGE_MODE')) {
try {
const { getBridgeAccessToken, getBridgeBaseUrl } = await import(
'src/bridge/bridgeConfig.js'
)
const { getSessionId } = await import('src/bootstrap/state.js')
const token = getBridgeAccessToken()
const sessionId = getSessionId()
if (token && sessionId) {
const baseUrl = getBridgeBaseUrl()
const axios = (await import('axios')).default
const response = await axios.post(
`${baseUrl}/v1/sessions/${sessionId}/events`,
{
events: [
{
type: 'push_notification',
title: input.title,
body: input.body,
priority: input.priority ?? 'normal',
},
],
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
},
timeout: 10_000,
validateStatus: (s: number) => s < 500,
},
)
if (response.status >= 200 && response.status < 300) {
logForDebugging(`[PushNotification] delivered via bridge session=${sessionId}`)
return { data: { sent: true } }
}
logForDebugging(`[PushNotification] bridge delivery failed: status=${response.status}`)
}
} catch (e) {
logForDebugging(`[PushNotification] bridge delivery error: ${e}`)
}
}
}
// Fallback: no bridge available, push was not delivered to a remote device.
logForDebugging(`[PushNotification] no bridge available, not delivered: ${input.title}`)
return { data: { sent: false, error: 'No Remote Control bridge configured. Notification not delivered.' } }
},
})

View File

@@ -70,14 +70,51 @@ Guidelines:
}
},
async call(_input: SendUserFileInput) {
// File transfer is handled by the KAIROS assistant transport layer.
// Without the KAIROS runtime, this tool is not available.
async call(input: SendUserFileInput, context) {
const { file_path } = input
const { stat } = await import('fs/promises')
// Verify file exists and is readable
let fileSize: number
try {
const fileStat = await stat(file_path)
if (!fileStat.isFile()) {
return {
data: { sent: false, file_path, error: 'Path is not a file.' },
}
}
fileSize = fileStat.size
} catch {
return {
data: { sent: false, file_path, error: 'File does not exist or is not readable.' },
}
}
// Attempt bridge upload if available (so web viewers can download)
const appState = context.getAppState()
let fileUuid: string | undefined
if (appState.replBridgeEnabled) {
try {
const { uploadBriefAttachment } = await import(
'@claude-code-best/builtin-tools/tools/BriefTool/upload.js'
)
fileUuid = await uploadBriefAttachment(file_path, fileSize, {
replBridgeEnabled: true,
signal: context.abortController.signal,
})
} catch {
// Best-effort upload — local path is always available
}
}
const delivered = !appState.replBridgeEnabled || Boolean(fileUuid)
return {
data: {
sent: false,
file_path: _input.file_path,
error: 'SendUserFile requires the KAIROS assistant transport layer.',
sent: delivered,
file_path,
size: fileSize,
...(fileUuid ? { file_uuid: fileUuid } : {}),
...(!delivered ? { error: 'Bridge upload failed. File available at local path.' } : {}),
},
}
},

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,10 @@
/** Thin logging wrapper — silent in test environment, uses console in production. */
const isTest = process.env.NODE_ENV === "test" || (typeof Bun !== "undefined" && !!Bun.env.BUN_TEST);
export function log(...args: unknown[]): void {
if (!isTest) console.log(...args);
}
export function error(...args: unknown[]): void {
if (!isTest) console.error(...args);
}

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { createBunWebSocket } from "hono/bun";
import { validateApiKey } from "../../auth/api-key";
@@ -30,14 +31,14 @@ function authenticateRequest(c: any, label: string, expectedSessionId?: string):
const payload = verifyWorkerJwt(token);
if (payload) {
if (expectedSessionId && payload.session_id !== expectedSessionId) {
console.log(`[Auth] ${label}: FAILED — JWT session_id mismatch`);
log(`[Auth] ${label}: FAILED — JWT session_id mismatch`);
return false;
}
return true;
}
}
console.log(`[Auth] ${label}: FAILED — no valid API key or JWT`);
log(`[Auth] ${label}: FAILED — no valid API key or JWT`);
return false;
}
@@ -85,7 +86,7 @@ app.get(
const session = getSession(sessionId);
if (!session) {
console.log(`[WS] Upgrade rejected: session ${sessionId} not found`);
log(`[WS] Upgrade rejected: session ${sessionId} not found`);
return {
onOpen(_evt, ws) {
ws.close(4001, "session not found");
@@ -93,7 +94,7 @@ app.get(
};
}
console.log(`[WS] Upgrade accepted: session=${sessionId}`);
log(`[WS] Upgrade accepted: session=${sessionId}`);
return {
onOpen(_evt, ws) {
handleWebSocketOpen(ws as any, sessionId);
@@ -110,7 +111,7 @@ app.get(
handleWebSocketClose(ws as any, sessionId, closeEvt?.code, closeEvt?.reason);
},
onError(evt, ws) {
console.error(`[WS] Error on session=${sessionId}:`, evt);
logError(`[WS] Error on session=${sessionId}:`, evt);
handleWebSocketClose(ws as any, sessionId, 1006, "websocket error");
},
};

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import {
createSession,
@@ -23,7 +24,7 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
try {
await createWorkItem(body.environment_id, session.id);
} catch (err) {
console.error(`[RCS] Failed to create work item: ${(err as Error).message}`);
logError(`[RCS] Failed to create work item: ${(err as Error).message}`);
}
}

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getSession, isSessionClosedStatus, resolveOwnedWebSessionId, updateSessionStatus } from "../../services/session";
@@ -44,9 +45,9 @@ app.post("/sessions/:id/events", uuidAuth, async (c) => {
const body = await c.req.json();
const eventType = body.type || "user";
console.log(`[RC-DEBUG] web -> server: POST /web/sessions/${sessionId}/events type=${eventType} content=${JSON.stringify(body).slice(0, 200)}`);
log(`[RC-DEBUG] web -> server: POST /web/sessions/${sessionId}/events type=${eventType} content=${JSON.stringify(body).slice(0, 200)}`);
const event = publishSessionEvent(sessionId, eventType, body, "outbound");
console.log(`[RC-DEBUG] web -> server: published outbound event id=${event.id} type=${event.type} direction=${event.direction} subscribers=${getEventBus(sessionId).subscriberCount()}`);
log(`[RC-DEBUG] web -> server: published outbound event id=${event.id} type=${event.type} direction=${event.direction} subscribers=${getEventBus(sessionId).subscriberCount()}`);
return c.json({ status: "ok", event }, 200);
});

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import {
@@ -35,7 +36,7 @@ app.post("/sessions", uuidAuth, async (c) => {
try {
await createWorkItem(body.environment_id, session.id);
} catch (err) {
console.error(`[RCS] Failed to create work item: ${(err as Error).message}`);
logError(`[RCS] Failed to create work item: ${(err as Error).message}`);
}
}

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../logger";
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
import { storeListSessions } from "../store";
import { config } from "../config";
@@ -10,7 +11,7 @@ export function runDisconnectMonitorSweep(now = Date.now()) {
const envs = storeListActiveEnvironments();
for (const env of envs) {
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
console.log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeUpdateEnvironment(env.id, { status: "disconnected" });
}
}
@@ -21,7 +22,7 @@ export function runDisconnectMonitorSweep(now = Date.now()) {
if (session.status === "running" || session.status === "idle") {
const elapsed = now - session.updatedAt.getTime();
if (elapsed > timeoutMs * 2) {
console.log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
updateSessionStatus(session.id, "inactive");
}
}

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../logger";
import {
storeCreateWorkItem,
storeGetWorkItem,
@@ -35,7 +36,7 @@ export async function createWorkItem(environmentId: string, sessionId: string):
const secret = encodeWorkSecret();
const record = storeCreateWorkItem({ environmentId, sessionId, secret });
console.log(`[RCS] Work item created: ${record.id} for env=${environmentId} session=${sessionId}`);
log(`[RCS] Work item created: ${record.id} for env=${environmentId} session=${sessionId}`);
return record.id;
}

View File

@@ -1,3 +1,5 @@
import { log, error as logError } from "../logger";
export interface SessionEvent {
id: string;
sessionId: string;
@@ -33,12 +35,12 @@ export class EventBus {
createdAt: Date.now(),
};
this.events.push(full);
console.log(`[RC-DEBUG] bus publish: sessionId=${event.sessionId} type=${event.type} dir=${event.direction} seq=${full.seqNum} subscribers=${this.subscribers.size}`);
log(`[RC-DEBUG] bus publish: sessionId=${event.sessionId} type=${event.type} dir=${event.direction} seq=${full.seqNum} subscribers=${this.subscribers.size}`);
for (const cb of this.subscribers) {
try {
cb(full);
} catch (err) {
console.error(`[RC-DEBUG] bus subscriber error:`, err);
logError(`[RC-DEBUG] bus subscriber error:`, err);
}
}
return full;

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../logger";
import type { Context } from "hono";
import type { SessionEvent } from "./event-bus";
import { getEventBus } from "./event-bus";
@@ -76,7 +77,7 @@ export function createSSEStream(c: Context, sessionId: string, fromSeqNum = 0) {
seqNum: event.seqNum,
});
try {
console.log(`[RC-DEBUG] SSE -> web: sessionId=${sessionId} type=${event.type} dir=${event.direction} seq=${event.seqNum}`);
log(`[RC-DEBUG] SSE -> web: sessionId=${sessionId} type=${event.type} dir=${event.direction} seq=${event.seqNum}`);
controller.enqueue(encoder.encode(`id: ${event.seqNum}\nevent: message\ndata: ${data}\n\n`));
} catch {
unsub();

View File

@@ -2,6 +2,7 @@ import type { WSContext } from "hono/ws";
import { getEventBus } from "./event-bus";
import type { SessionEvent } from "./event-bus";
import { publishSessionEvent } from "../services/transport";
import { log, error as logError } from "../logger";
// Per-connection cleanup, keyed by sessionId (only one WS per session)
interface CleanupEntry {
@@ -97,13 +98,13 @@ function toSDKMessage(event: SessionEvent): string {
/** Called from onOpen — subscribes to event bus, forwards outbound events to bridge WS */
export function handleWebSocketOpen(ws: WSContext, sessionId: string) {
const openTime = Date.now();
console.log(`[RC-DEBUG] [WS] Open session=${sessionId}`);
log(`[RC-DEBUG] [WS] Open session=${sessionId}`);
activeConnections.add(ws);
// If there's an existing connection for this session, clean it up first
const existing = cleanupBySession.get(sessionId);
if (existing) {
console.log(`[WS] Replacing existing connection for session=${sessionId}`);
log(`[WS] Replacing existing connection for session=${sessionId}`);
existing.unsub();
clearInterval(existing.keepalive);
activeConnections.delete(existing.ws);
@@ -115,7 +116,7 @@ export function handleWebSocketOpen(ws: WSContext, sessionId: string) {
// the full conversation history — assistant replies are inbound events.
const missed = bus.getEventsSince(0);
if (missed.length > 0) {
console.log(`[WS] Replaying ${missed.length} missed event(s)`);
log(`[WS] Replaying ${missed.length} missed event(s)`);
for (const event of missed) {
if (ws.readyState !== 1) break;
try {
@@ -131,10 +132,10 @@ export function handleWebSocketOpen(ws: WSContext, sessionId: string) {
if (event.direction !== "outbound") return;
try {
const sdkMsg = toSDKMessage(event);
console.log(`[RC-DEBUG] [WS] -> bridge (outbound): type=${event.type} len=${sdkMsg.length} msg=${sdkMsg.slice(0, 300)}`);
log(`[RC-DEBUG] [WS] -> bridge (outbound): type=${event.type} len=${sdkMsg.length} msg=${sdkMsg.slice(0, 300)}`);
ws.send(sdkMsg);
} catch (err) {
console.error("[RC-DEBUG] [WS] send error:", err);
logError("[RC-DEBUG] [WS] send error:", err);
}
});
@@ -162,7 +163,7 @@ export function handleWebSocketMessage(ws: WSContext, sessionId: string, data: s
try {
ingestBridgeMessage(sessionId, JSON.parse(line));
} catch (err) {
console.error("[WS] parse error:", err);
logError("[WS] parse error:", err);
}
}
}
@@ -174,7 +175,7 @@ export function handleWebSocketClose(ws: WSContext, sessionId: string, code?: nu
const entry = cleanupBySession.get(sessionId);
const duration = entry ? Math.round((Date.now() - entry.openTime) / 1000) : -1;
console.log(`[WS] Close session=${sessionId} code=${code ?? "none"} reason=${reason || "(none)"} duration=${duration}s`);
log(`[WS] Close session=${sessionId} code=${code ?? "none"} reason=${reason || "(none)"} duration=${duration}s`);
if (entry) {
entry.unsub();
@@ -216,7 +217,7 @@ export function ingestBridgeMessage(sessionId: string, msg: Record<string, unkno
const eventType = deriveEventType(msg);
console.log(`[RC-DEBUG] [WS] <- bridge (inbound): sessionId=${sessionId} type=${eventType}${msg.uuid ? ` uuid=${msg.uuid}` : ""} msg=${JSON.stringify(msg).slice(0, 300)}`);
log(`[RC-DEBUG] [WS] <- bridge (inbound): sessionId=${sessionId} type=${eventType}${msg.uuid ? ` uuid=${msg.uuid}` : ""} msg=${JSON.stringify(msg).slice(0, 300)}`);
let payload: unknown;
@@ -256,7 +257,7 @@ export function closeAllConnections(): void {
const count = activeConnections.size;
if (count === 0) return;
console.log(`[WS] Gracefully closing ${count} active connection(s)...`);
log(`[WS] Gracefully closing ${count} active connection(s)...`);
for (const [sessionId, entry] of cleanupBySession) {
try {
entry.unsub();
@@ -270,5 +271,5 @@ export function closeAllConnections(): void {
}
cleanupBySession.clear();
activeConnections.clear();
console.log("[WS] All connections closed");
log("[WS] All connections closed");
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "web"]
}

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}