From e87f022dd42ab08f5929162a2bd742495e2e01a6 Mon Sep 17 00:00:00 2001 From: dengm Date: Sat, 16 May 2026 18:22:28 +0800 Subject: [PATCH 001/212] feat: add manual MCP server reconnect with secondary menu Replace automatic retry with user-initiated reconnect: - Failed servers show error details and a [Reconnect] option - Reconnect reads latest config from disk (no restart needed) - Single attempt per reconnect, no backoff/retry --- src/mcp/mcp-client.ts | 34 ++++- src/mcp/mcp-manager.ts | 266 +++++++++++++++++++++++--------------- src/session.ts | 4 + src/tests/session.test.ts | 55 ++++++++ src/ui/App.tsx | 9 +- src/ui/McpStatusList.tsx | 129 ++++++++++++------ 6 files changed, 346 insertions(+), 151 deletions(-) diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index 96367328..3651c888 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -106,19 +106,24 @@ export class McpClient { >(); private stderrBuffer = ""; private notificationHandler: McpNotificationHandler | null = null; + private disconnectHandler: ((reason: string) => void) | null = null; + private intentionallyDisconnected = false; constructor( private readonly serverName: string, private readonly command: string, private readonly args: string[] = [], private readonly env?: Record, - onNotification?: McpNotificationHandler + onNotification?: McpNotificationHandler, + onDisconnect?: (reason: string) => void ) { this.notificationHandler = onNotification ?? null; + this.disconnectHandler = onDisconnect ?? null; } async connect(timeoutMs: number): Promise { return new Promise((resolve, reject) => { + this.intentionallyDisconnected = false; const childEnv = { ...process.env, ...this.env, @@ -144,17 +149,35 @@ export class McpClient { }); } + let resolved = false; + const safeReject = (err: Error) => { + if (!resolved) { + resolved = true; + reject(err); + } + }; + this.process.on("error", (err) => { - reject(this.withStderr(`Failed to start MCP server "${this.serverName}" (${this.command}): ${err.message}`)); + safeReject( + this.withStderr(`Failed to start MCP server "${this.serverName}" (${this.command}): ${err.message}`) + ); }); this.process.on("close", (code) => { - const error = this.withStderr(`MCP server "${this.serverName}" exited with code ${code}`); + const reason = `MCP server "${this.serverName}" exited with code ${code}`; + const error = this.withStderr(reason); for (const [, pending] of this.pendingRequests) { clearTimeout(pending.timer); pending.reject(error); } this.pendingRequests.clear(); + this.reader?.close(); + this.reader = null; + this.process = null; + if (!this.intentionallyDisconnected && this.disconnectHandler) { + this.disconnectHandler(reason); + } + safeReject(error); }); if (this.process.stderr) { @@ -263,6 +286,7 @@ export class McpClient { } disconnect(): void { + this.intentionallyDisconnected = true; if (this.reader) { this.reader.close(); this.reader = null; @@ -273,6 +297,10 @@ export class McpClient { } } + isConnected(): boolean { + return this.process !== null && this.process.exitCode === null; + } + private sendRequest(method: string, params: Record, timeoutMs = 30_000): Promise { return new Promise((resolve, reject) => { const id = this.nextId++; diff --git a/src/mcp/mcp-manager.ts b/src/mcp/mcp-manager.ts index 5a9f5530..217e3fc1 100644 --- a/src/mcp/mcp-manager.ts +++ b/src/mcp/mcp-manager.ts @@ -1,7 +1,9 @@ import { McpClient, type McpToolDefinition, type McpPromptDefinition, type McpResourceDefinition } from "./mcp-client"; import type { McpServerConfig } from "../settings"; -const MCP_STARTUP_TIMEOUT_MS = 30_000; +const MCP_STARTUP_TIMEOUT_MS = process.env.DEEPCODE_MCP_TIMEOUT + ? parseInt(process.env.DEEPCODE_MCP_TIMEOUT, 10) + : 30_000; const MCP_CALL_TOOL_TIMEOUT_MS = 60_000; type McpToolEntry = { @@ -14,7 +16,7 @@ type McpToolEntry = { export type McpServerStatus = { name: string; - status: "starting" | "ready" | "failed"; + status: "starting" | "ready" | "failed" | "reconnecting"; connected: boolean; error?: string; toolCount: number; @@ -46,12 +48,10 @@ export class McpManager { private serverStatuses: McpServerStatus[] = []; private onToolsListChanged: (() => void) | null = null; private onStatusChanged: (() => void) | null = null; + private serverConfigs: Record = {}; prepare(servers?: Record): void { if (!servers || Object.keys(servers).length === 0) return; - // Clear the disposed flag — a re-prepare means we are live again. - // (disconnect() sets disposed=true to stop a stale initialize() loop, - // but prepare+initialize must be able to start a new one.) this.disposed = false; for (const name of Object.keys(servers)) { @@ -81,116 +81,175 @@ export class McpManager { if (!servers || Object.keys(servers).length === 0) return; - const entries = Object.entries(servers); + this.serverConfigs = servers; this.prepare(servers); - for (const [name, config] of entries) { + for (const [name, config] of Object.entries(servers)) { if (this.disposed) break; - let client: McpClient | null = null; - try { - client = new McpClient(name, config.command, config.args ?? [], config.env, (method) => { + await this.connectServer(name, config); + } + } + + async reconnect(name: string, config?: McpServerConfig): Promise { + if (this.disposed) return; + const effectiveConfig = config ?? this.serverConfigs[name]; + if (!effectiveConfig) return; + if (config) { + this.serverConfigs[name] = config; + } + + this.setStatus({ + name, + status: "reconnecting", + connected: false, + error: "Reconnecting...", + toolCount: 0, + tools: [], + promptCount: 0, + prompts: [], + resourceCount: 0, + resources: [], + }); + + await this.connectServer(name, effectiveConfig); + } + + private async connectServer(name: string, config: McpServerConfig): Promise { + if (this.disposed) return; + + // Clean up stale entries from previous connection attempts + this.clients = this.clients.filter((c) => c.isConnected()); + this.tools = this.tools.filter((t) => t.serverName !== name); + this.prompts = this.prompts.filter((p) => p.serverName !== name); + this.resources = this.resources.filter((r) => r.serverName !== name); + + let client: McpClient | null = null; + try { + client = new McpClient( + name, + config.command, + config.args ?? [], + config.env, + (method) => { if (method === "notifications/tools/list_changed") { - this.refreshServerTools(name, client!).catch(() => { - // swallow refresh errors - }); + this.refreshServerTools(name, client!).catch(() => {}); + } + }, + (reason) => { + if (!this.disposed && this.serverConfigs[name]) { + this.onServerCrash(name, reason); } - }); - await client.connect(MCP_STARTUP_TIMEOUT_MS); - if (this.disposed) { - client.disconnect(); - break; - } - this.clients.push(client); - - // Discover tools - const serverTools = await client.listTools(MCP_STARTUP_TIMEOUT_MS); - if (this.disposed) break; - const toolNamespacedNames: string[] = []; - for (const tool of serverTools) { - const namespacedName = `mcp__${name}__${tool.name}`; - this.tools.push({ - serverName: name, - originalName: tool.name, - namespacedName, - definition: tool, - client, - }); - toolNamespacedNames.push(namespacedName); - } - - // Discover prompts - let serverPrompts: McpPromptDefinition[] = []; - try { - serverPrompts = await client.listPrompts(MCP_STARTUP_TIMEOUT_MS); - } catch { - // Server may not support prompts — safe to ignore - } - if (this.disposed) break; - const promptNamespacedNames: string[] = []; - for (const prompt of serverPrompts) { - const namespacedName = `mcp__${name}__${prompt.name}`; - this.prompts.push({ - serverName: name, - namespacedName, - definition: prompt, - client, - }); - promptNamespacedNames.push(namespacedName); } + ); + await client.connect(MCP_STARTUP_TIMEOUT_MS); + if (this.disposed) { + client.disconnect(); + return; + } + this.clients.push(client); - // Discover resources - let serverResources: McpResourceDefinition[] = []; - try { - serverResources = await client.listResources(MCP_STARTUP_TIMEOUT_MS); - } catch { - // Server may not support resources — safe to ignore - } - if (this.disposed) break; - const resourceNamespacedNames: string[] = []; - for (const resource of serverResources) { - const namespacedName = `mcp__${name}__${resource.name}`; - this.resources.push({ - serverName: name, - namespacedName, - definition: resource, - client, - }); - resourceNamespacedNames.push(namespacedName); - } + const serverTools = await client.listTools(MCP_STARTUP_TIMEOUT_MS); + if (this.disposed) return; + const toolNamespacedNames: string[] = []; + for (const tool of serverTools) { + const namespacedName = `mcp__${name}__${tool.name}`; + this.tools.push({ + serverName: name, + originalName: tool.name, + namespacedName, + definition: tool, + client, + }); + toolNamespacedNames.push(namespacedName); + } - this.setStatus({ - name, - status: "ready", - connected: true, - toolCount: serverTools.length, - tools: toolNamespacedNames, - promptCount: serverPrompts.length, - prompts: promptNamespacedNames, - resourceCount: serverResources.length, - resources: resourceNamespacedNames, + let serverPrompts: McpPromptDefinition[] = []; + try { + serverPrompts = await client.listPrompts(MCP_STARTUP_TIMEOUT_MS); + } catch { + // server may not support prompts + } + if (this.disposed) return; + const promptNamespacedNames: string[] = []; + for (const prompt of serverPrompts) { + const namespacedName = `mcp__${name}__${prompt.name}`; + this.prompts.push({ + serverName: name, + namespacedName, + definition: prompt, + client, }); - } catch (err) { - if (this.disposed) break; - client?.disconnect(); - const message = err instanceof Error ? err.message : String(err); - // 不在控制台输出错误日志,避免暴露敏感信息 - // process.stderr.write(`[deepcode] MCP server "${name}" failed to initialize: ${message}\n`); - this.setStatus({ - name, - status: "failed", - connected: false, - error: message, - toolCount: 0, - tools: [], - promptCount: 0, - prompts: [], - resourceCount: 0, - resources: [], + promptNamespacedNames.push(namespacedName); + } + + let serverResources: McpResourceDefinition[] = []; + try { + serverResources = await client.listResources(MCP_STARTUP_TIMEOUT_MS); + } catch { + // server may not support resources + } + if (this.disposed) return; + const resourceNamespacedNames: string[] = []; + for (const resource of serverResources) { + const namespacedName = `mcp__${name}__${resource.name}`; + this.resources.push({ + serverName: name, + namespacedName, + definition: resource, + client, }); + resourceNamespacedNames.push(namespacedName); } + + this.setStatus({ + name, + status: "ready", + connected: true, + toolCount: serverTools.length, + tools: toolNamespacedNames, + promptCount: serverPrompts.length, + prompts: promptNamespacedNames, + resourceCount: serverResources.length, + resources: resourceNamespacedNames, + }); + } catch (err) { + client?.disconnect(); + const message = err instanceof Error ? err.message : String(err); + this.setStatus({ + name, + status: "failed", + connected: false, + error: message, + toolCount: 0, + tools: [], + promptCount: 0, + prompts: [], + resourceCount: 0, + resources: [], + }); } } + private onServerCrash(name: string, reason: string): void { + if (this.disposed) return; + this.clients = this.clients.filter((c) => c.isConnected()); + this.tools = this.tools.filter((t) => t.serverName !== name); + this.prompts = this.prompts.filter((p) => p.serverName !== name); + this.resources = this.resources.filter((r) => r.serverName !== name); + this.setStatus({ + name, + status: "failed", + connected: false, + error: reason, + toolCount: 0, + tools: [], + promptCount: 0, + prompts: [], + resourceCount: 0, + resources: [], + }); + } + getStatus(): McpServerStatus[] { const result = [...this.serverStatuses]; const knownNames = new Set(result.map((s) => s.name)); @@ -345,12 +404,12 @@ export class McpManager { this.resources = []; this.serverStatuses = []; this.configuredServerNames = []; + this.serverConfigs = {}; this.initialized = false; } private async refreshServerTools(serverName: string, client: McpClient): Promise { const serverTools = await client.listTools(MCP_STARTUP_TIMEOUT_MS); - // Remove old tool entries for this server this.tools = this.tools.filter((t) => t.serverName !== serverName); const toolNamespacedNames: string[] = []; for (const tool of serverTools) { @@ -364,13 +423,11 @@ export class McpManager { }); toolNamespacedNames.push(namespacedName); } - // Update status const existing = this.serverStatuses.find((s) => s.name === serverName); if (existing) { existing.toolCount = serverTools.length; existing.tools = toolNamespacedNames; } - // Notify listener this.onToolsListChanged?.(); } @@ -390,7 +447,6 @@ export class McpManager { } else { this.serverStatuses[index] = status; } - // 触发状态变更回调 this.onStatusChanged?.(); } } diff --git a/src/session.ts b/src/session.ts index 095cd3aa..9e97f861 100644 --- a/src/session.ts +++ b/src/session.ts @@ -255,6 +255,10 @@ export class SessionManager { return this.mcpManager.getStatus(); } + async reconnectMcpServer(name: string, config?: McpServerConfig): Promise { + await this.mcpManager.reconnect(name, config); + } + dispose(): void { this.mcpManager.disconnect(); } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 50d016cc..8ecb85e1 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1540,6 +1540,61 @@ test("SessionManager treats OpenAI APIUserAbortError as interrupted", async () = assert.equal(session?.failReason, "interrupted"); }); +test("SessionManager marks MCP server as failed on single failed attempt (no auto-retry)", async () => { + const workspace = createTempDir("deepcode-mcp-fail-noworkspace-"); + const serverPath = path.join(workspace, "mcp-server-fail.cjs"); + fs.writeFileSync(serverPath, "process.exit(7);", "utf8"); + + const manager = createSessionManager(workspace, "machine-id-mcp-fail-no"); + await manager.initMcpServers({ broken: { command: process.execPath, args: [serverPath] } }); + + const status = manager.getMcpStatus(); + assert.equal(status.length, 1); + assert.equal(status[0]?.status, "failed"); + assert.match(status[0]?.error ?? "", /exited with code 7/); + + manager.dispose(); +}); + +test("SessionManager reconnect succeeds on previously failed server", async () => { + const workspace = createTempDir("deepcode-mcp-reconn-ok-workspace-"); + const serverPath = path.join(workspace, "mcp-server-ok.cjs"); + fs.writeFileSync( + serverPath, + ` +const readline = require("readline"); +const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); +function send(message) { + process.stdout.write(JSON.stringify(message) + "\\n"); +} +rl.on("line", (line) => { + const request = JSON.parse(line); + if (!("id" in request)) return; + if (request.method === "initialize") { + send({ jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2024-11-05", capabilities: {} } }); + return; + } + if (request.method === "tools/list") { + send({ jsonrpc: "2.0", id: request.id, result: { tools: [{ name: "ping", inputSchema: { type: "object", properties: {} } }] } }); + return; + } + send({ jsonrpc: "2.0", id: request.id, result: { content: [] } }); +}); +`, + "utf8" + ); + + const manager = createSessionManager(workspace, "machine-id-mcp-reconn-ok"); + await manager.initMcpServers({ fixable: { command: process.execPath, args: [serverPath] } }); + + const status = manager.getMcpStatus(); + assert.equal(status.length, 1); + assert.equal(status[0]?.status, "ready"); + assert.equal(status[0]?.toolCount, 1); + + manager.dispose(); +}); + function createSessionManager(projectRoot: string, machineId: string): SessionManager { return new SessionManager({ projectRoot, diff --git a/src/ui/App.tsx b/src/ui/App.tsx index bafb4120..1f121980 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -455,7 +455,14 @@ export function App({ projectRoot, version = "", onRestart }: AppProps): React.R onCancel={() => setView("chat")} /> ) : view === "mcp-status" ? ( - setView("chat")} /> + setView("chat")} + onReconnect={(name) => { + const latest = resolveCurrentSettings(projectRoot); + void sessionManager.reconnectMcpServer(name, latest.mcpServers?.[name]); + }} + /> ) : shouldShowQuestionPrompt && pendingQuestion && !busy ? ( void; + onReconnect: (name: string) => void; }; -export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement { +export function McpStatusList({ statuses, onCancel, onReconnect }: Props): React.ReactElement { const { columns, rows } = useWindowSize(); // 视图模式:server-list(服务器列表) 或 server-detail(服务器详情) @@ -20,10 +21,10 @@ export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement setViewMode("server-list"); }, []); - // 进入服务器详情 + // 进入服务器详情(允许 ready、failed、reconnecting 状态) const enterDetail = useCallback(() => { const server = statuses[selectedServerIndex]; - if (server && server.status === "ready") { + if (server && (server.status === "ready" || server.status === "failed" || server.status === "reconnecting")) { setViewMode("server-detail"); } }, [statuses, selectedServerIndex]); @@ -59,6 +60,7 @@ export function McpStatusList({ statuses, onCancel }: Props): React.ReactElement server={statuses[selectedServerIndex]} onBack={goBack} onCancel={onCancel} + onReconnect={onReconnect} rows={rows} columns={columns} /> @@ -173,6 +175,7 @@ function ServerListView({ const readyCount = statuses.filter((s) => s.status === "ready").length; const startingCount = statuses.filter((s) => s.status === "starting").length; + const reconnectingCount = statuses.filter((s) => s.status === "reconnecting").length; const failedCount = statuses.filter((s) => s.status === "failed").length; return ( @@ -198,6 +201,11 @@ function ServerListView({ {startingCount} starting, + {reconnectingCount > 0 && ( + + {reconnectingCount} reconnecting, + + )} {failedCount} failed @@ -257,15 +265,23 @@ function ServerRow({ selected: boolean; labelColumnWidth: number; }): React.ReactElement { - const icon = status.status === "ready" ? "✓" : status.status === "failed" ? "✗" : "●"; - const color = status.status === "ready" ? "green" : status.status === "failed" ? "red" : "yellow"; + const icon = + status.status === "ready" ? "✓" : status.status === "failed" ? "✗" : status.status === "reconnecting" ? "↻" : "●"; + const color = + status.status === "ready" + ? "green" + : status.status === "failed" + ? "red" + : status.status === "reconnecting" + ? "#ff9900" + : "yellow"; // 加载动画:循环显示 (空) → . → .. → ... → (空) → ... const [dots, setDots] = React.useState(0); React.useEffect(() => { - if (status.status !== "starting") return; + if (status.status !== "starting" && status.status !== "reconnecting") return; const interval = setInterval(() => { - setDots((d) => (d + 1) % 4); // 0 → 1 → 2 → 3 → 0 ... + setDots((d) => (d + 1) % 4); }, 500); return () => clearInterval(interval); }, [status.status]); @@ -275,7 +291,9 @@ function ServerRow({ ? `Ready (${status.toolCount} tools, ${status.promptCount} prompts, ${status.resourceCount} resources)` : status.status === "failed" ? `Failed` - : "Starting" + (dots > 0 ? ".".repeat(dots) : " "); // 动态显示 (空) / . / .. / ... + : status.status === "reconnecting" + ? `Reconnecting${dots > 0 ? ".".repeat(dots) : " "}` + : "Starting" + (dots > 0 ? ".".repeat(dots) : " "); return ( @@ -293,8 +311,10 @@ function ServerRow({ - {/* Error message for failed servers */} - {status.status === "failed" && status.error ? : null} + {/* Error message for failed or reconnecting servers */} + {(status.status === "failed" || status.status === "reconnecting") && status.error ? ( + + ) : null} ); } @@ -304,59 +324,54 @@ function ServerDetailView({ server, onBack, onCancel, + onReconnect, rows, columns, }: { server: McpServerStatus; onBack: () => void; onCancel: () => void; + onReconnect: (name: string) => void; rows: number; columns: number; }): React.ReactElement { - const [activeIndex, setActiveIndex] = useState(0); + const [activeIndex, setActiveIndex] = React.useState(0); + const hasReconnect = server.status === "failed"; + const canScroll = server.status === "ready"; - // 合并所有 items(tools, prompts, resources) + // 合并所有 items(tools, prompts, resources)+ Reconnect 选项 const allItems = useMemo(() => { const items: { type: string; name: string }[] = []; + if (hasReconnect) { + items.push({ type: "action", name: "Reconnect" }); + } server.tools.forEach((tool) => items.push({ type: "tool", name: tool })); server.prompts.forEach((prompt) => items.push({ type: "prompt", name: prompt })); server.resources.forEach((resource) => items.push({ type: "resource", name: resource })); return items; - }, [server]); + }, [server, hasReconnect]); const totalItems = allItems.length; const maxVisible = useMemo(() => { - const reservedLines = 10; // header + title + stats + footer + borders + const reservedLines = 12; // header + title + stats + error + footer + borders const availableLines = Math.max(0, Math.min(rows, 30) - reservedLines); return Math.max(1, availableLines); }, [rows]); - // 使用 ref 跟踪 visibleStart,避免循环依赖 const visibleStartRef = React.useRef(0); - // 计算可见窗口起始位置:当 activeIndex 超出可见区域时才滚动(类似终端光标行为) const visibleStart = useMemo(() => { if (totalItems === 0) return 0; - const currentStart = visibleStartRef.current; let newStart = currentStart; - - // 如果 activeIndex 在当前可见窗口之前,滚动到 activeIndex if (activeIndex < currentStart) { newStart = activeIndex; - } - // 如果 activeIndex 在当前可见窗口之后,滚动到 activeIndex - else if (activeIndex >= currentStart + maxVisible) { + } else if (activeIndex >= currentStart + maxVisible) { newStart = activeIndex - maxVisible + 1; } - - // 限制在合法范围内 newStart = Math.max(0, Math.min(newStart, Math.max(0, totalItems - maxVisible))); - - // 更新 ref visibleStartRef.current = newStart; - return newStart; }, [activeIndex, maxVisible, totalItems]); @@ -371,11 +386,16 @@ function ServerDetailView({ onBack(); return; } - // Space 或 Enter 键返回一级菜单 - if (input === " " || key.return) { + if (key.return || input === " ") { + if (activeIndex === 0 && hasReconnect) { + onReconnect(server.name); + onBack(); + return; + } onBack(); return; } + if (!canScroll && !hasReconnect) return; if (key.upArrow) { setActiveIndex((prev) => Math.max(0, prev - 1)); return; @@ -384,25 +404,33 @@ function ServerDetailView({ setActiveIndex((prev) => Math.min(totalItems - 1, prev + 1)); return; } - if (key.pageUp) { + if (key.pageUp && canScroll) { setActiveIndex((prev) => Math.max(0, prev - maxVisible)); return; } - if (key.pageDown) { + if (key.pageDown && canScroll) { setActiveIndex((prev) => Math.min(totalItems - 1, prev + maxVisible)); return; } - if (key.home) { + if (key.home && canScroll) { setActiveIndex(0); return; } - if (key.end) { + if (key.end && canScroll) { setActiveIndex(totalItems - 1); } }); - const icon = "✓"; - const color = "green"; + const statusIcon = + server.status === "ready" ? "✓" : server.status === "failed" ? "✗" : server.status === "reconnecting" ? "↻" : "●"; + const statusColor = + server.status === "ready" + ? "green" + : server.status === "failed" + ? "red" + : server.status === "reconnecting" + ? "#ff9900" + : "yellow"; return ( {/* Header row */} - {icon} + {statusIcon} {server.name} - — Details + — {server.status === "ready" ? "Details" : "Status"} {/* Server info */} - {server.toolCount} tools, {server.promptCount} prompts, {server.resourceCount} resources + {server.status === "ready" + ? `${server.toolCount} tools, ${server.promptCount} prompts, ${server.resourceCount} resources` + : `Status: ${server.status}`} + {/* Error for failed/reconnecting */} + {server.error && (server.status === "failed" || server.status === "reconnecting") ? ( + + + + ) : null} {/* Items list */} {/* Footer */} - ↑/↓ scroll · Space/Enter back · Esc back · Ctrl+C close + + {hasReconnect + ? "Enter to reconnect · Esc back · Ctrl+C close" + : canScroll + ? "↑/↓ scroll · Space/Enter back · Esc back · Ctrl+C close" + : "Space/Enter back · Esc back · Ctrl+C close"} + @@ -481,13 +523,16 @@ function ServerDetailView({ } function ItemRow({ item, selected }: { item: { type: string; name: string }; selected: boolean }): React.ReactElement { - const icon = item.type === "tool" ? "🔧" : item.type === "prompt" ? "📝" : "📦"; + const isAction = item.type === "action"; + const icon = isAction ? "↻" : item.type === "tool" ? "🔧" : item.type === "prompt" ? "📝" : "📦"; + const color = isAction && selected ? "#ff9900" : selected ? "#229ac3" : undefined; return ( + {selected ? "> " : " "} {icon} - - {item.name} + + {isAction ? `[${item.name}]` : item.name} ); From 52dafba25903dc70258d7e59dbe86e283a0f091f Mon Sep 17 00:00:00 2001 From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com> Date: Mon, 18 May 2026 09:50:38 +0800 Subject: [PATCH 002/212] fix: re-apply dynamic modifier parsing for Shift+Enter after upstream sync Upstream v0.1.21 reverted PR #70. Re-apply: - isShiftReturn() / isReturn() dynamic CSI modifier bit parsing - Kitty progressive enhancement (ESC[>1u) alongside xterm modifyOtherKeys - Clear input when key.return is true (safety net) --- src/tests/promptInputKeys.test.ts | 6 ++--- src/ui/prompt/cursor.ts | 4 +-- src/ui/prompt/useTerminalInput.ts | 43 ++++++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts index 69d20758..8952a3d9 100644 --- a/src/tests/promptInputKeys.test.ts +++ b/src/tests/promptInputKeys.test.ts @@ -80,7 +80,7 @@ test("parseTerminalInput keeps BS payload for meta+backspace", () => { test("parseTerminalInput recognizes shifted return sequences", () => { const { input, key } = parseTerminalInput("\u001B\r"); - assert.equal(input, "\r"); + assert.equal(input, ""); assert.equal(key.return, true); assert.equal(key.shift, true); assert.equal(key.meta, false); @@ -108,8 +108,8 @@ test("parseTerminalInput recognizes alternate shifted return sequences", () => { }); test("terminal extended key helpers request and restore modifyOtherKeys mode", () => { - assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m"); - assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m"); + assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m\u001B[>1u"); + assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m\u001B[ { diff --git a/src/ui/prompt/cursor.ts b/src/ui/prompt/cursor.ts index 2668470c..59b24f23 100644 --- a/src/ui/prompt/cursor.ts +++ b/src/ui/prompt/cursor.ts @@ -41,11 +41,11 @@ function disableTerminalFocusReporting(): string { } export function enableTerminalExtendedKeys(): string { - return "\u001B[>4;1m"; + return "\u001B[>4;1m\u001B[>1u"; } export function disableTerminalExtendedKeys(): string { - return "\u001B[>4;0m"; + return "\u001B[>4;0m\u001B[ Date: Mon, 18 May 2026 10:22:22 +0800 Subject: [PATCH 003/212] fix: refresh mcpToolDefinitions cache after MCP reconnect After reconnectMcpServer succeeds, SessionManager's cached mcpToolDefinitions was stale, causing "Unknown MCP tool" errors when the model tried to call reconnected tools. --- src/session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/session.ts b/src/session.ts index eddfe5c8..0527ba8b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -261,6 +261,7 @@ export class SessionManager { async reconnectMcpServer(name: string, config?: McpServerConfig): Promise { await this.mcpManager.reconnect(name, config); + this.mcpToolDefinitions = this.mcpManager.getMcpToolDefinitions(); } dispose(): void { From 47d3c21abe3c3582d24e7c1109bdf19e0818c90d Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 18 May 2026 18:13:34 +0800 Subject: [PATCH 004/212] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=20/raw?= =?UTF-8?q?=20=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E7=BB=84=E4=BB=B6=E5=92=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 RawMode 功能,包括 Normal、Lite 和 Raw scrollback 模式 - App 组件中集成 RawMode 上下文及切换逻辑,支持在 Raw 模式下直接向 stdout 渲染消息 - 增加 RawModeExitPrompt 组件,支持按 ESC 退出原始模式 - 新增 RawModelDropdown 组件,提供原始模式选择下拉菜单 - 在 PromptInput 中集成原始模式选择交互及状态管理 - 调整消息视图实现,拆分 MessageView 到 compoments 目录,支持根据 RawMode 呈现不同内容 - 新建 AppContainer 组件,包装 App 并提供版本上下文和 RawModeProvider - 修改 SlashCommand 体系,支持内置 /raw 命令及对应测试覆盖 - 更新 cli 入口,使用 AppContainer 替换直接渲染 App,传递版本信息 - 移除旧 MessageView 文件,重构消息渲染逻辑 - 优化 SlashCommandMenu 显示,支持命令参数提示显示 - 更新相关测试,支持原始模式功能验证 --- src/cli.tsx | 4 +- src/tests/messageView.test.ts | 51 +-- src/tests/slashCommands.test.ts | 9 +- src/ui/App.tsx | 69 +++- src/ui/AppContainer.tsx | 21 ++ src/ui/MessageView.tsx | 355 ------------------ src/ui/PromptInput.tsx | 27 +- src/ui/SlashCommandMenu.tsx | 5 +- src/ui/WelcomeScreen.tsx | 11 +- src/ui/compoments/MessageView/index.tsx | 183 +++++++++ .../{ => compoments/MessageView}/markdown.ts | 0 src/ui/compoments/MessageView/types.ts | 19 + src/ui/compoments/MessageView/utils.ts | 255 +++++++++++++ src/ui/compoments/RawModeExitPrompt/index.tsx | 15 + src/ui/compoments/RawModelDropdown/index.tsx | 55 +++ src/ui/compoments/index.ts | 3 + src/ui/contexts/AppContext.tsx | 15 + src/ui/contexts/RawModeContext.tsx | 40 ++ src/ui/contexts/index.ts | 3 + src/ui/index.ts | 5 +- src/ui/slashCommands.ts | 22 +- 21 files changed, 750 insertions(+), 417 deletions(-) create mode 100644 src/ui/AppContainer.tsx delete mode 100644 src/ui/MessageView.tsx create mode 100644 src/ui/compoments/MessageView/index.tsx rename src/ui/{ => compoments/MessageView}/markdown.ts (100%) create mode 100644 src/ui/compoments/MessageView/types.ts create mode 100644 src/ui/compoments/MessageView/utils.ts create mode 100644 src/ui/compoments/RawModeExitPrompt/index.tsx create mode 100644 src/ui/compoments/RawModelDropdown/index.tsx create mode 100644 src/ui/compoments/index.ts create mode 100644 src/ui/contexts/AppContext.tsx create mode 100644 src/ui/contexts/RawModeContext.tsx create mode 100644 src/ui/contexts/index.ts diff --git a/src/cli.tsx b/src/cli.tsx index 435499a9..e8e8659e 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,8 +1,8 @@ import React from "react"; import { render } from "ink"; -import { App } from "./ui"; import { setShellIfWindows } from "./common/shell-utils"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./updateCheck"; +import AppContainer from "./ui/AppContainer"; const args = process.argv.slice(2); const packageInfo = readPackageInfo(); @@ -81,7 +81,7 @@ async function main(): Promise { const appInitialPrompt = initialPrompt; initialPrompt = undefined; const inkInstance = render( - { const lines = parseDiffPreview( @@ -25,45 +26,29 @@ test("parseDiffPreview keeps nonstandard context lines", () => { test("MessageView summarizes thinking content across lines", () => { assert.equal( - getThinkingParams({ - content: "Plan:\n\nInspect the code and update tests", - }), + buildThinkingSummary("Plan:\n\nInspect the code and update tests", null, RawMode.Lite), "Plan: Inspect the code and update tests" ); }); -test("MessageView removes a trailing colon from thinking summaries", () => { - assert.equal(getThinkingParams({ content: "Planning:" }), "Planning"); +test("MessageView removes a trailing colon from thinking summary", () => { + assert.equal(buildThinkingSummary("Planning:", null, RawMode.Lite), "Planning"); }); -test("MessageView falls back to a reasoning placeholder for hidden reasoning content", () => { +test("MessageView falls back to a reasoning placeholder for hidden reasoning content in Lite mode", () => { assert.equal( - getThinkingParams({ - content: "", - messageParams: { reasoning_content: "hidden chain of thought" }, - }), + buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Lite), "(reasoning...)" ); }); -function getThinkingParams(overrides: Partial): string { - const view = MessageView({ message: buildAssistantMessage(overrides) }) as any; - return view.props.children.props.params; -} - -function buildAssistantMessage(overrides: Partial): SessionMessage { - return { - id: "message-1", - sessionId: "session-1", - role: "assistant", - content: "", - contentParams: null, - messageParams: null, - compacted: false, - visible: true, - createTime: "2026-01-01T00:00:00.000Z", - updateTime: "2026-01-01T00:00:00.000Z", - meta: { asThinking: true }, - ...overrides, - }; -} +test("MessageView shows full reasoning content in Normal/Raw mode", () => { + assert.equal( + buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.None), + "hidden chain of thought" + ); + assert.equal( + buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Raw), + "hidden chain of thought" + ); +}); diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts index bba52447..34b48d01 100644 --- a/src/tests/slashCommands.test.ts +++ b/src/tests/slashCommands.test.ts @@ -19,7 +19,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => { assert.equal(items[0].kind, "skill"); assert.equal(items[0].name, "skill-writer"); const builtinNames = items.filter((i) => i.kind !== "skill").map((i) => i.name); - assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "continue", "mcp", "exit"]); + assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "continue", "mcp", "raw", "exit"]); }); test("filterSlashCommands matches partial prefixes", () => { @@ -80,6 +80,13 @@ test("findExactSlashCommand returns built-in /model", () => { assert.equal(item?.kind, "model"); }); +test("findExactSlashCommand returns built-in /raw", () => { + const items = buildSlashCommands(skills); + const item = findExactSlashCommand(items, "/raw"); + assert.ok(item); + assert.equal(item?.kind, "raw"); +}); + test("findExactSlashCommand returns the matching skill", () => { const items = buildSlashCommands(skills); const item = findExactSlashCommand(items, "/code-review"); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index e56111f0..1c9bac42 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -6,10 +6,10 @@ import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; import { - SessionManager, type LlmStreamProgress, type MessageMeta, type SessionEntry, + SessionManager, type SessionMessage, type SessionStatus, type SkillInfo, @@ -17,13 +17,13 @@ import { } from "../session"; import { applyModelConfigSelection, - resolveSettingsSources, type DeepcodingSettings, type ModelConfigSelection, type ResolvedDeepcodingSettings, + resolveSettingsSources, } from "../settings"; import { PromptInput, type PromptSubmission } from "./PromptInput"; -import { MessageView } from "./MessageView"; +import { MessageView, RawModeExitPrompt } from "./compoments"; import { SessionList } from "./SessionList"; import { buildLoadingText } from "./loadingText"; import { findExpandedThinkingId } from "./thinkingState"; @@ -32,11 +32,13 @@ import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; import { McpStatusList } from "./McpStatusList"; import { ProcessStdoutView } from "./ProcessStdoutView"; import { + type AskUserQuestionAnswers, findPendingAskUserQuestion, formatAskUserQuestionAnswers, - type AskUserQuestionAnswers, } from "./askUserQuestion"; import { buildExitSummaryText } from "./exitSummary"; +import { RawMode, useRawModeContext } from "./contexts"; +import { renderMessageToStdout } from "./compoments/MessageView/utils"; const DEFAULT_MODEL = "deepseek-v4-pro"; const DEFAULT_BASE_URL = "https://api.deepseek.com"; @@ -45,12 +47,11 @@ type View = "chat" | "session-list" | "mcp-status"; type AppProps = { projectRoot: string; - version?: string; initialPrompt?: string; onRestart?: () => void; }; -export function App({ projectRoot, version = "", initialPrompt, onRestart }: AppProps): React.ReactElement { +export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement { const { exit } = useApp(); const { stdout, write } = useStdout(); const { columns } = useWindowSize(); @@ -75,6 +76,10 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App const [showProcessStdout, setShowProcessStdout] = useState(false); const processStdoutRef = useRef>(new Map()); + const { mode, setMode } = useRawModeContext(); + const rawModeRef = useRef(mode); + rawModeRef.current = mode; + const messagesRef = useRef([]); messagesRef.current = messages; @@ -86,6 +91,10 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App renderMarkdown: (text) => text, onAssistantMessage: (message: SessionMessage) => { setMessages((prev) => [...prev, message]); + if (rawModeRef.current === RawMode.Raw) { + process.stdout.write("\n"); + process.stdout.write(renderMessageToStdout(message, rawModeRef.current) + "\n\n"); + } }, onSessionEntryUpdated: (entry) => { setStatusLine(buildStatusLine(entry)); @@ -362,6 +371,39 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App [sessionManager, refreshSkills] ); + const handleRawModeChange = useCallback( + (nextMode: string) => { + const activeSessionId = sessionManager.getActiveSessionId(); + if (!activeSessionId) { + return; + } + + setMode(nextMode as RawMode); + + // Clear screen to remove stale formatted text. + process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); + + setTimeout(() => { + if (nextMode === RawMode.Raw) { + // Write all messages directly to stdout for raw scrollback mode. + const allMessages = loadVisibleMessages(sessionManager, activeSessionId); + for (const msg of allMessages) { + process.stdout.write("\n"); + process.stdout.write(renderMessageToStdout(msg, nextMode) + "\n\n"); + } + if (allMessages.length > 0) { + process.stdout.write("\n\n"); + process.stdout.write(chalk.dim("Press ESC to exit raw mode")); + } + } else { + // Switch to chat view to render messages. + handleSelectSession(activeSessionId); + } + }, 200); + }, + [handleSelectSession, sessionManager, setMode] + ); + const [stableColumns, setStableColumns] = useState(columns); useEffect(() => { const timer = setTimeout(() => setStableColumns(columns), 100); @@ -413,7 +455,7 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App // eslint-disable-next-line react-hooks/exhaustive-deps -- nowTick forces periodic recalculation for spinner animation [busy, streamProgress, runningProcesses, nowTick] ); - const welcomeSettings = resolvedSettings; + const welcomeItem: SessionMessage = useMemo( () => ({ id: `__welcome__${welcomeNonce}`, @@ -430,11 +472,14 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App [welcomeNonce] ); const staticItems = useMemo(() => { + if (mode === RawMode.Raw) { + return []; + } if (showWelcome && view === "chat") { return [welcomeItem, ...messages]; } return messages; - }, [showWelcome, view, messages, welcomeItem]); + }, [mode, showWelcome, view, messages, welcomeItem]); const handleQuestionAnswers = useCallback( (answers: AskUserQuestionAnswers) => { @@ -453,6 +498,10 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App setDismissedQuestionIds((prev) => new Set(prev).add(pendingQuestion.messageId)); }, [pendingQuestion]); + if (mode === RawMode.Raw) { + return handleRawModeChange(RawMode.None)} />; + } + return ( @@ -462,9 +511,8 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App ); @@ -521,6 +569,7 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App runningProcesses={runningProcesses} onSubmit={handleSubmit} onModelConfigChange={handleModelConfigChange} + onRawModeChange={handleRawModeChange} onInterrupt={handleInterrupt} onToggleProcessStdout={handleToggleProcessStdout} placeholder="Type your message..." diff --git a/src/ui/AppContainer.tsx b/src/ui/AppContainer.tsx new file mode 100644 index 00000000..e437b44a --- /dev/null +++ b/src/ui/AppContainer.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { AppContext } from "./contexts"; +import { App } from "./App"; +import { RawModeProvider } from "./contexts/RawModeContext"; + +const AppContainer: React.FC<{ + projectRoot: string; + version: string; + initialPrompt: string | undefined; + onRestart: () => void; +}> = ({ version, projectRoot, initialPrompt, onRestart }) => { + return ( + + + + + + ); +}; + +export default AppContainer; diff --git a/src/ui/MessageView.tsx b/src/ui/MessageView.tsx deleted file mode 100644 index c8793fc5..00000000 --- a/src/ui/MessageView.tsx +++ /dev/null @@ -1,355 +0,0 @@ -import React from "react"; -import { Box, Text } from "ink"; -import { renderMarkdown } from "./markdown"; -import type { SessionMessage } from "../session"; - -type Props = { - message: SessionMessage; - collapsed?: boolean; - width?: number; -}; - -export function MessageView({ message, collapsed, width = 80 }: Props): React.ReactElement | null { - if (!message.visible) { - return null; - } - - if (message.role === "user") { - const text = message.content || "(no content)"; - return ( - - - {`>`} - - - {text} - {Array.isArray(message.contentParams) && message.contentParams.length > 0 ? ( - {` 📎 ${message.contentParams.length} image attachment(s)`} - ) : null} - - - ); - } - - if (message.role === "assistant") { - const isThinking = Boolean(message.meta?.asThinking); - const content = (message.content || "").trim(); - - if (isThinking) { - const summary = buildThinkingSummary(content, message.messageParams); - if (collapsed !== false) { - return ( - - - - ); - } - return ( - - - - {content ? {renderMarkdown(content)} : null} - - - ); - } - - const containerWidth = Math.max(1, width - 2); - const contentWidth = Math.max(1, width - 4); - - return ( - - - - - - {content ? {renderMarkdown(content)} : null} - - - ); - } - - if (message.role === "tool") { - const summary = buildToolSummary(message); - const diffLines = getToolDiffPreviewLines(summary); - return ( - - - {diffLines.length > 0 ? : null} - - ); - } - - if (message.role === "system") { - // Render model change messages in the same style as user commands. - if (message.meta?.isModelChange) { - return ( - - - {`>`} - - - {message.content} - - - ); - } - - if (message.meta?.skill) { - return ( - - ⚡ Loaded skill: {message.meta.skill.name} - - ); - } - if (message.meta?.isSummary) { - return ( - - - (conversation summary inserted) - - - ); - } - return null; - } - - return null; -} - -function StatusLine({ - bulletColor, - name, - params, -}: { - bulletColor: "gray" | "green" | "red"; - name: string; - params: string; -}): React.ReactElement { - return ( - - {[ - - ✧ - , - " ", - - {name} - , - params ? {` ${params}`} : null, - ]} - - ); -} - -function formatToolStatusParams(summary: ToolSummary): string { - const params = firstNonEmptyLine(summary.params); - return summary.name.toLowerCase() === "bash" ? params : truncate(params, 120); -} - -type ToolSummary = { - name: string; - params: string; - ok: boolean; - metadata: Record | null; -}; - -type DiffPreviewLine = { - marker: string; - content: string; - kind: "added" | "removed" | "context"; -}; - -function buildToolSummary(message: SessionMessage): ToolSummary { - const payload = parseToolPayload(message.content); - const metaFunctionName = - message.meta?.function && typeof (message.meta.function as { name?: unknown }).name === "string" - ? (message.meta.function as { name: string }).name - : null; - const name = payload.name || metaFunctionName || "tool"; - const params = - name === "AskUserQuestion" - ? extractAskUserQuestionParams(message) || getMetaParams(message) - : getMetaParams(message); - - return { - name, - params, - ok: payload.ok !== false, - metadata: payload.metadata, - }; -} - -function getMetaParams(message: SessionMessage): string { - return typeof message.meta?.paramsMd === "string" ? message.meta.paramsMd.trim() : ""; -} - -function extractAskUserQuestionParams(message: SessionMessage): string { - const fromFunction = extractQuestionsFromToolFunction(message.meta?.function); - if (fromFunction) { - return fromFunction; - } - - const params = getMetaParams(message); - if (!params) { - return ""; - } - - try { - const parsed = JSON.parse(params); - return extractQuestionsFromValue(parsed); - } catch { - return ""; - } -} - -function extractQuestionsFromToolFunction(toolFunction: unknown): string { - if (!toolFunction || typeof toolFunction !== "object") { - return ""; - } - const args = (toolFunction as { arguments?: unknown }).arguments; - if (typeof args !== "string" || !args.trim()) { - return ""; - } - try { - const parsed = JSON.parse(args); - return extractQuestionsFromValue((parsed as { questions?: unknown })?.questions); - } catch { - return ""; - } -} - -function extractQuestionsFromValue(value: unknown): string { - if (!Array.isArray(value)) { - return ""; - } - return value - .map((item) => { - if (!item || typeof item !== "object" || Array.isArray(item)) { - return ""; - } - return typeof (item as { question?: unknown }).question === "string" - ? (item as { question: string }).question.trim() - : ""; - }) - .filter(Boolean) - .join(" / "); -} - -function parseToolPayload(content: string | null): { - name: string | null; - ok: boolean; - metadata: Record | null; -} { - if (!content) { - return { name: null, ok: true, metadata: null }; - } - - try { - const parsed = JSON.parse(content) as { name?: unknown; ok?: unknown; metadata?: unknown }; - return { - name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : null, - ok: parsed.ok !== false, - metadata: isPlainRecord(parsed.metadata) ? parsed.metadata : null, - }; - } catch { - return { name: null, ok: true, metadata: null }; - } -} - -function getToolDiffPreviewLines(summary: ToolSummary): DiffPreviewLine[] { - if (!summary.ok || !["edit", "write"].includes(summary.name.toLowerCase())) { - return []; - } - const diffPreview = summary.metadata?.diff_preview; - if (typeof diffPreview !== "string" || !diffPreview.trim()) { - return []; - } - return parseDiffPreview(diffPreview); -} - -export function parseDiffPreview(diffPreview: string): DiffPreviewLine[] { - return diffPreview - .split("\n") - .filter((line) => line && !line.startsWith("--- ") && !line.startsWith("+++ ") && !line.startsWith("@@ ")) - .map((line) => { - if (line.startsWith("+")) { - return { marker: "+", content: line.slice(1), kind: "added" }; - } - if (line.startsWith("-")) { - return { marker: "-", content: line.slice(1), kind: "removed" }; - } - return { - marker: " ", - content: line.startsWith(" ") ? line.slice(1) : line, - kind: "context", - }; - }); -} - -function DiffPreview({ lines }: { lines: DiffPreviewLine[] }): React.ReactElement { - return ( - - └ Changes - - {lines.map((line, index) => ( - - - {line.marker} - - - {line.content} - - - ))} - - - ); -} - -function isPlainRecord(value: unknown): value is Record { - return Boolean(value && typeof value === "object" && !Array.isArray(value)); -} - -function formatStatusName(value: string): string { - return value ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : "Tool"; -} - -function truncate(value: string, max: number): string { - if (value.length <= max) { - return value; - } - return `${value.slice(0, max)}…`; -} - -function firstNonEmptyLine(value: string): string { - for (const line of value.split(/\r?\n/)) { - const trimmed = line.trim().replace(/\s+/g, " "); - if (trimmed) { - return trimmed; - } - } - return ""; -} - -function buildThinkingSummary(content: string, messageParams: unknown | null): string { - if (content) { - const normalized = content.replace(/\r?\n/g, " ").replace(/\s+/g, " "); - let result = truncate(normalized, 100); - if (result.endsWith(":") || result.endsWith(":")) { - result = result.slice(0, -1); - } - return result; - } - - const params = messageParams as { reasoning_content?: unknown } | null | undefined; - if (typeof params?.reasoning_content === "string" && params.reasoning_content.trim()) { - return "(reasoning...)"; - } - - return ""; -} diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index affa9ad1..c1cf3356 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -43,6 +43,7 @@ import { useHiddenTerminalCursor, useTerminalExtendedKeys, useTerminalFocusRepor import SlashCommandMenu from "./SlashCommandMenu"; import type { ModelConfigSelection, ReasoningEffort } from "../settings"; import DropdownMenu from "./DropdownMenu"; +import { RawModelDropdown } from "./compoments"; export type PromptSubmission = { text: string; @@ -63,6 +64,7 @@ type Props = { runningProcesses?: Map | null; onSubmit: (submission: PromptSubmission) => void; onModelConfigChange: (selection: ModelConfigSelection) => string | Promise; + onRawModeChange?: (mode: string) => void; onInterrupt: () => void; onToggleProcessStdout?: () => void; }; @@ -116,6 +118,7 @@ export const PromptInput = React.memo(function PromptInput({ onModelConfigChange, onInterrupt, onToggleProcessStdout, + onRawModeChange, }: Props): React.ReactElement { const { exit } = useApp(); const { stdout } = useStdout(); @@ -126,6 +129,7 @@ export const PromptInput = React.memo(function PromptInput({ const [pendingExit, setPendingExit] = useState(false); const [menuIndex, setMenuIndex] = useState(0); const [showSkillsDropdown, setShowSkillsDropdown] = useState(false); + const [openRawModelDropdown, setOpenRawModelDropdown] = useState(false); const [skillsDropdownIndex, setSkillsDropdownIndex] = useState(0); const [modelDropdownStep, setModelDropdownStep] = useState(null); const [modelDropdownIndex, setModelDropdownIndex] = useState(0); @@ -271,6 +275,10 @@ export const PromptInput = React.memo(function PromptInput({ setPendingExit(false); } + if (openRawModelDropdown) { + return; + } + if (historyCursor !== -1 && !key.upArrow && !key.downArrow) { exitHistoryBrowsing(); } @@ -607,6 +615,11 @@ export const PromptInput = React.memo(function PromptInput({ openModelDropdown(); return; } + if (item.kind === "raw") { + clearSlashToken(); + setOpenRawModelDropdown(true); + return; + } if (item.kind === "new") { onSubmit({ text: "", imageUrls: [], command: "new" }); setBuffer(EMPTY_BUFFER); @@ -760,10 +773,13 @@ export const PromptInput = React.memo(function PromptInput({ })); const showFooterText = useMemo( - () => showMenu || showSkillsDropdown || modelDropdownStep !== null, - [showMenu, showSkillsDropdown, modelDropdownStep] + () => showMenu || showSkillsDropdown || openRawModelDropdown || modelDropdownStep !== null, + [showMenu, showSkillsDropdown, openRawModelDropdown, modelDropdownStep] ); + const matchedCommand = slashToken ? findExactSlashCommand(slashItems, slashToken) : null; + const inlineHint = matchedCommand?.args ? ` ${matchedCommand.args.join("|")}` : ""; + return ( {imageUrls.length > 0 ? ( @@ -791,7 +807,14 @@ export const PromptInput = React.memo(function PromptInput({ > {renderBufferWithCursor(buffer, !disabled && hasTerminalFocus, placeholder)} + {inlineHint ? {inlineHint} : null} + onRawModeChange?.(mode)} + screenWidth={screenWidth} + /> {showSkillsDropdown ? ( s.label.length)); + const longestLabel = Math.max(...items.map((s) => s.label.length + (s.args ? s.args?.join("|")?.length + 4 : 0))); const contentWidth = longestLabel + 2; // +2 for prefix "> " or " " const maxAllowed = Math.max(10, (width - 2) >> 1); // 容器50%宽度(减去gap),至少保留10列 return Math.min(contentWidth, maxAllowed); @@ -49,11 +49,12 @@ const SlashCommandMenu = React.memo(function SlashCommandMenu({ const actualIndex = visibleStart + idx; return ( - + {actualIndex === activeIndex ? "> " : " "} {formatSlashCommandLabel(item)} + {item.args ? {item.args.join("|")} : null} diff --git a/src/ui/WelcomeScreen.tsx b/src/ui/WelcomeScreen.tsx index 3d82eed0..7e740d1f 100644 --- a/src/ui/WelcomeScreen.tsx +++ b/src/ui/WelcomeScreen.tsx @@ -7,12 +7,12 @@ import type { ResolvedDeepcodingSettings } from "../settings"; import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./slashCommands"; import { ThemedGradient } from "./ThemedGradient"; import { AsciiLogo } from "../AsciiArt"; +import { useAppContext } from "./contexts"; type WelcomeScreenProps = { projectRoot: string; settings: ResolvedDeepcodingSettings; skills: SkillInfo[]; - version: string; width: number; }; @@ -28,13 +28,8 @@ const SHORTCUT_TIPS = [ { label: "Ctrl+D twice", description: "Quit Deep Code CLI" }, ]; -export function WelcomeScreen({ - projectRoot, - settings, - skills, - version, - width, -}: WelcomeScreenProps): React.ReactElement { +export function WelcomeScreen({ projectRoot, settings, skills, width }: WelcomeScreenProps): React.ReactElement { + const { version } = useAppContext(); const tips = useMemo(() => buildWelcomeTips(skills), [skills]); const [tipIndex] = useState(() => randomTipIndex(tips.length)); const compact = width < TITLE_PANEL_WIDTH + 42; diff --git a/src/ui/compoments/MessageView/index.tsx b/src/ui/compoments/MessageView/index.tsx new file mode 100644 index 00000000..9aa82fd0 --- /dev/null +++ b/src/ui/compoments/MessageView/index.tsx @@ -0,0 +1,183 @@ +import React from "react"; +import { Box, Text } from "ink"; +import { renderMarkdown } from "./markdown"; +import { + buildThinkingSummary, + buildToolSummary, + formatStatusName, + formatToolStatusParams, + getToolDiffPreviewLines, +} from "./utils"; +import type { DiffPreviewLine, MessageViewProps } from "./types"; +import { RawMode, useRawModeContext } from "../../contexts"; + +export function MessageView({ message, collapsed, width = 80 }: MessageViewProps): React.ReactElement | null { + const { mode } = useRawModeContext(); + if (!message.visible) { + return null; + } + + if (message.role === "user") { + const text = message.content || "(no content)"; + return ( + + + {`>`} + + + {text} + {Array.isArray(message.contentParams) && message.contentParams.length > 0 ? ( + {` 📎 ${message.contentParams.length} image attachment(s)`} + ) : null} + + + ); + } + + if (message.role === "assistant") { + const isThinking = Boolean(message.meta?.asThinking); + const content = (message.content || "").trim(); + + if (isThinking) { + const summary = buildThinkingSummary(content, message.messageParams, mode); + if (collapsed !== false) { + return ( + + + + ); + } + return ( + + + + {content ? {renderMarkdown(content)} : null} + + + ); + } + + const containerWidth = Math.max(1, width - 2); + const contentWidth = Math.max(1, width - 4); + + return ( + + + + + + {content ? {renderMarkdown(content)} : null} + + + ); + } + + if (message.role === "tool") { + const summary = buildToolSummary(message); + const diffLines = getToolDiffPreviewLines(summary); + return ( + + + {diffLines.length > 0 ? : null} + + ); + } + + if (message.role === "system") { + // Render model change messages in the same style as user commands. + if (message.meta?.isModelChange) { + return ( + + + {`>`} + + + {message.content} + + + ); + } + + if (message.meta?.skill) { + return ( + + ⚡ Loaded skill: {message.meta.skill.name} + + ); + } + if (message.meta?.isSummary) { + return ( + + + (conversation summary inserted) + + + ); + } + return null; + } + + return null; +} + +function StatusLine({ + bulletColor, + name, + params, + width, +}: { + bulletColor: "gray" | "green" | "red"; + name: string; + params: string; + width: number; +}): React.ReactElement { + const { mode } = useRawModeContext(); + const containerWidth = Math.max(1, width - 2); + const contentWidth = Math.max(1, width - 4); + return ( + + + + ✧ + + + + + + {name} + + {params ? ( + + {` ${params}`} + + ) : null} + + + + ); +} + +function DiffPreview({ lines }: { lines: DiffPreviewLine[] }): React.ReactElement { + return ( + + └ Changes + + {lines.map((line, index) => ( + + + {line.marker} + + + {line.content} + + + ))} + + + ); +} diff --git a/src/ui/markdown.ts b/src/ui/compoments/MessageView/markdown.ts similarity index 100% rename from src/ui/markdown.ts rename to src/ui/compoments/MessageView/markdown.ts diff --git a/src/ui/compoments/MessageView/types.ts b/src/ui/compoments/MessageView/types.ts new file mode 100644 index 00000000..743eb2dc --- /dev/null +++ b/src/ui/compoments/MessageView/types.ts @@ -0,0 +1,19 @@ +import type { SessionMessage } from "../../../session"; + +export type MessageViewProps = { + message: SessionMessage; + collapsed?: boolean; + width?: number; +}; +export type ToolSummary = { + name: string; + params: string; + ok: boolean; + metadata: Record | null; +}; + +export type DiffPreviewLine = { + marker: string; + content: string; + kind: "added" | "removed" | "context"; +}; diff --git a/src/ui/compoments/MessageView/utils.ts b/src/ui/compoments/MessageView/utils.ts new file mode 100644 index 00000000..50a7b946 --- /dev/null +++ b/src/ui/compoments/MessageView/utils.ts @@ -0,0 +1,255 @@ +import type { DiffPreviewLine, ToolSummary } from "./types"; +import type { SessionMessage } from "../../../session"; +import { RawMode } from "../../contexts"; +import chalk from "chalk"; + +/** Type guard that checks whether a value is a plain object (not null, not an array). */ +export function isPlainRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + +/** Capitalizes the first character of a tool status name, falling back to "Tool". */ +export function formatStatusName(value: string): string { + return value ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : "Tool"; +} + +/** Truncates a string to the given maximum length, appending an ellipsis when truncated. */ +export function truncate(value: string, max: number): string { + if (value.length <= max) { + return value; + } + return `${value.slice(0, max)}…`; +} + +/** Returns the first non-empty line from a multi-line string, normalizing whitespace. */ +export function firstNonEmptyLine(value: string): string { + for (const line of value.split(/\r?\n/)) { + const trimmed = line.trim().replace(/\s+/g, " "); + if (trimmed) { + return trimmed; + } + } + return ""; +} + +/** + * Builds a one-line summary of thinking / reasoning content. + * Falls back to "(reasoning...)" when only reasoning_content params are present. + */ +export function buildThinkingSummary(content: string, messageParams: unknown | null, mode?: RawMode): string { + if (content) { + const normalized = content.replace(/\r?\n/g, " ").replace(/\s+/g, " "); + let result = truncate(normalized, 100); + if (result.endsWith(":") || result.endsWith(":")) { + result = result.slice(0, -1); + } + return result; + } + + const params = messageParams as { reasoning_content?: unknown } | null | undefined; + if (typeof params?.reasoning_content === "string" && params.reasoning_content.trim()) { + return mode !== RawMode.Lite ? params?.reasoning_content || "" : "(reasoning...)"; + } + + return ""; +} + +/** Formats a tool's parameters for status display, preserving full bash commands but truncating others. */ +export function formatToolStatusParams(summary: ToolSummary): string { + const params = firstNonEmptyLine(summary.params); + return summary.name.toLowerCase() === "bash" ? params : truncate(params, 120); +} + +/** Builds a structured summary (name, params, ok, metadata) from a tool session message. */ +export function buildToolSummary(message: SessionMessage): ToolSummary { + const payload = parseToolPayload(message.content); + const metaFunctionName = + message.meta?.function && typeof (message.meta.function as { name?: unknown }).name === "string" + ? (message.meta.function as { name: string }).name + : null; + const name = payload.name || metaFunctionName || "tool"; + const params = + name === "AskUserQuestion" + ? extractAskUserQuestionParams(message) || getMetaParams(message) + : getMetaParams(message); + + return { + name, + params, + ok: payload.ok !== false, + metadata: payload.metadata, + }; +} + +/** Extracts the paramsMd field from a session message's metadata, trimmed. */ +export function getMetaParams(message: SessionMessage): string { + return typeof message.meta?.paramsMd === "string" ? message.meta.paramsMd.trim() : ""; +} + +/** + * Extracts human-readable question text from an AskUserQuestion tool message. + * Tries the tool function arguments first, then falls back to parsing metadata params. + */ +export function extractAskUserQuestionParams(message: SessionMessage): string { + const fromFunction = extractQuestionsFromToolFunction(message.meta?.function); + if (fromFunction) { + return fromFunction; + } + + const params = getMetaParams(message); + if (!params) { + return ""; + } + + try { + const parsed = JSON.parse(params); + return extractQuestionsFromValue(parsed); + } catch { + return ""; + } +} + +/** + * Extracts question strings from a tool function object by parsing its JSON arguments. + */ +export function extractQuestionsFromToolFunction(toolFunction: unknown): string { + if (!toolFunction || typeof toolFunction !== "object") { + return ""; + } + const args = (toolFunction as { arguments?: unknown }).arguments; + if (typeof args !== "string" || !args.trim()) { + return ""; + } + try { + const parsed = JSON.parse(args); + return extractQuestionsFromValue((parsed as { questions?: unknown })?.questions); + } catch { + return ""; + } +} + +/** Extracts and joins question strings from an array of question objects. */ +export function extractQuestionsFromValue(value: unknown): string { + if (!Array.isArray(value)) { + return ""; + } + return value + .map((item) => { + if (!item || typeof item !== "object" || Array.isArray(item)) { + return ""; + } + return typeof (item as { question?: unknown }).question === "string" + ? (item as { question: string }).question.trim() + : ""; + }) + .filter(Boolean) + .join(" / "); +} + +/** Parses a tool's JSON payload, extracting name, ok flag, and metadata. */ +export function parseToolPayload(content: string | null): { + name: string | null; + ok: boolean; + metadata: Record | null; +} { + if (!content) { + return { name: null, ok: true, metadata: null }; + } + + try { + const parsed = JSON.parse(content) as { name?: unknown; ok?: unknown; metadata?: unknown }; + return { + name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : null, + ok: parsed.ok !== false, + metadata: isPlainRecord(parsed.metadata) ? parsed.metadata : null, + }; + } catch { + return { name: null, ok: true, metadata: null }; + } +} + +/** + * Returns structured diff preview lines for successful edit or write tool calls. + * Returns an empty array if the tool is not edit/write or has no diff_preview metadata. + */ +export function getToolDiffPreviewLines(summary: ToolSummary): DiffPreviewLine[] { + if (!summary.ok || !["edit", "write"].includes(summary.name.toLowerCase())) { + return []; + } + const diffPreview = summary.metadata?.diff_preview; + if (typeof diffPreview !== "string" || !diffPreview.trim()) { + return []; + } + return parseDiffPreview(diffPreview); +} + +/** Parses a unified-diff-style preview string into an array of structured diff lines. */ +export function parseDiffPreview(diffPreview: string): DiffPreviewLine[] { + return diffPreview + .split("\n") + .filter((line) => line && !line.startsWith("--- ") && !line.startsWith("+++ ") && !line.startsWith("@@ ")) + .map((line) => { + if (line.startsWith("+")) { + return { marker: "+", content: line.slice(1), kind: "added" }; + } + if (line.startsWith("-")) { + return { marker: "-", content: line.slice(1), kind: "removed" }; + } + return { + marker: " ", + content: line.startsWith(" ") ? line.slice(1) : line, + kind: "context", + }; + }); +} + +export function renderMessageToStdout(message: SessionMessage, mode: RawMode): string { + if (!message.visible) { + return ""; + } + + if (message.role === "user") { + const text = message.content || "(no content)"; + return chalk(`> ${text}`); + } + + if (message.role === "assistant") { + const isThinking = Boolean(message.meta?.asThinking); + const content = (message.content || "").trim(); + + if (isThinking) { + const summary = buildThinkingSummary(content, message.messageParams, mode); + return `${chalk("✧")} ${chalk("Thinking")}${summary ? ` ${chalk(summary)}` : ""}`; + } + + return `${chalk("✦")} ${content}`; + } + + if (message.role === "tool") { + const payload = parseToolPayload(message.content); + const metaFunctionName = + message.meta?.function && typeof (message.meta.function as { name?: unknown }).name === "string" + ? (message.meta.function as { name: string }).name + : null; + const name = payload.name || metaFunctionName || "tool"; + const metaParams = typeof message.meta?.paramsMd === "string" ? message.meta.paramsMd.trim() : ""; + const params = name.toLowerCase() === "bash" ? metaParams : truncate(metaParams, 120); + return `${chalk("✧")} ${chalk(formatStatusName(name))}${params ? ` ${chalk(params)}` : ""}`; + } + + if (message.role === "system") { + if (message.meta?.isModelChange) { + return chalk(`> ${message.content}`); + } + if (message.meta?.skill && typeof message.meta.skill === "object") { + const skillName = (message.meta.skill as { name?: unknown }).name; + return chalk(`⚡ Loaded skill: ${typeof skillName === "string" ? skillName : ""}`); + } + if (message.meta?.isSummary) { + return chalk.dim.italic("(conversation summary inserted)"); + } + return ""; + } + + return ""; +} diff --git a/src/ui/compoments/RawModeExitPrompt/index.tsx b/src/ui/compoments/RawModeExitPrompt/index.tsx new file mode 100644 index 00000000..9b1d218b --- /dev/null +++ b/src/ui/compoments/RawModeExitPrompt/index.tsx @@ -0,0 +1,15 @@ +import type React from "react"; +import { useInput } from "ink"; + +export function RawModeExitPrompt({ onExit }: { onExit: () => void }): React.ReactElement | null { + useInput( + (_input, key) => { + if (key.escape) { + onExit(); + } + }, + { isActive: true } + ); + + return null; +} diff --git a/src/ui/compoments/RawModelDropdown/index.tsx b/src/ui/compoments/RawModelDropdown/index.tsx new file mode 100644 index 00000000..33970136 --- /dev/null +++ b/src/ui/compoments/RawModelDropdown/index.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import { useInput } from "ink"; +import DropdownMenu from "../../DropdownMenu"; +import type { RawMode } from "../../contexts"; +import { RAW_COMMAND_MODELS, useRawModeContext } from "../../contexts"; + +const RawModelDropdown: React.FC<{ + open: boolean; + screenWidth: number; + onClose?: (value: boolean) => void; + onSelect?: (model: string) => void; +}> = ({ open = false, screenWidth, onSelect, onClose }) => { + const { mode, setMode } = useRawModeContext(); + const [index, setIndex] = useState(0); + useInput( + (input, key) => { + if (key.upArrow) { + setIndex((i) => Math.max(0, i - 1)); + return; + } + if (key.downArrow) { + setIndex((i) => Math.min(RAW_COMMAND_MODELS.length - 1, i + 1)); + return; + } + if ((input === " " && !key.ctrl && !key.meta) || (key.return && !key.shift && !key.meta)) { + setMode(RAW_COMMAND_MODELS[index].key as RawMode); + onClose?.(false); + onSelect?.(RAW_COMMAND_MODELS[index].key); + return; + } + if (key.escape) { + onClose?.(false); + return; + } + }, + { isActive: open } + ); + if (!open) { + return null; + } + return ( + ({ ...model, selected: model.key === mode }))} + helpText="Space/Enter select mode · Esc to close" + // onSelect={onSelect} + activeColor="#229ac3" + maxVisible={6} + activeIndex={index} + width={screenWidth} + /> + ); +}; + +export default RawModelDropdown; diff --git a/src/ui/compoments/index.ts b/src/ui/compoments/index.ts new file mode 100644 index 00000000..942d3ed1 --- /dev/null +++ b/src/ui/compoments/index.ts @@ -0,0 +1,3 @@ +export { default as RawModelDropdown } from "./RawModelDropdown"; +export { MessageView } from "./MessageView"; +export { RawModeExitPrompt } from "./RawModeExitPrompt"; diff --git a/src/ui/contexts/AppContext.tsx b/src/ui/contexts/AppContext.tsx new file mode 100644 index 00000000..34d45894 --- /dev/null +++ b/src/ui/contexts/AppContext.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from "react"; + +export interface AppState { + version: string; +} + +export const AppContext = createContext(null); + +export const useAppContext = () => { + const context = useContext(AppContext); + if (!context) { + throw new Error("useAppContext must be used within an AppProvider"); + } + return context; +}; diff --git a/src/ui/contexts/RawModeContext.tsx b/src/ui/contexts/RawModeContext.tsx new file mode 100644 index 00000000..a7b60904 --- /dev/null +++ b/src/ui/contexts/RawModeContext.tsx @@ -0,0 +1,40 @@ +import React, { createContext, useContext, useState } from "react"; +import type { DropdownMenuItem } from "../DropdownMenu"; + +export enum RawMode { + None = "Normal mode", + Lite = "Lite mode", + Raw = "Raw scrollback mode", +} +export const RAW_COMMAND_MODELS: DropdownMenuItem[] = [ + { + label: "Lite mode", + key: RawMode.Lite, + }, + { + label: "Raw scrollback mode", + key: RawMode.Raw, + }, + { + label: "Normal mode", + key: RawMode.None, + }, +] as const; + +const RawModeContext = createContext<{ mode: RawMode; setMode: React.Dispatch> }>({ + mode: RawMode.Lite, + setMode: () => {}, +}); + +export function useRawModeContext() { + const context = useContext(RawModeContext); + if (!context) { + throw new Error("useRawModeContext must be used within a RawModeProvider"); + } + return context; +} + +export const RawModeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [mode, setMode] = useState(RawMode.Lite); + return {children}; +}; diff --git a/src/ui/contexts/index.ts b/src/ui/contexts/index.ts new file mode 100644 index 00000000..37e40cdb --- /dev/null +++ b/src/ui/contexts/index.ts @@ -0,0 +1,3 @@ +export { AppContext, useAppContext } from "./AppContext"; +export type { AppState } from "./AppContext"; +export { RawMode, RAW_COMMAND_MODELS, useRawModeContext, RawModeProvider } from "./RawModeContext"; diff --git a/src/ui/index.ts b/src/ui/index.ts index 5b4ff8f3..dd99330c 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -9,7 +9,8 @@ export { createOpenAIClient, } from "./App"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; -export { MessageView, parseDiffPreview } from "./MessageView"; +export { MessageView } from "./compoments"; +export { parseDiffPreview } from "./compoments/MessageView/utils"; export { PromptInput, IMAGE_ATTACHMENT_CLEAR_HINT, @@ -47,7 +48,7 @@ export { } from "./askUserQuestion"; export { readClipboardImage, type ClipboardImage } from "./clipboard"; export { buildLoadingText, type LoadingTextInput } from "./loadingText"; -export { renderMarkdown } from "./markdown"; +export { renderMarkdown } from "./compoments/MessageView/markdown"; export { EMPTY_BUFFER, insertText, diff --git a/src/ui/slashCommands.ts b/src/ui/slashCommands.ts index 6552ba09..aab06bd6 100644 --- a/src/ui/slashCommands.ts +++ b/src/ui/slashCommands.ts @@ -1,6 +1,16 @@ import type { SkillInfo } from "../session"; -export type SlashCommandKind = "skill" | "skills" | "model" | "new" | "init" | "resume" | "continue" | "mcp" | "exit"; +export type SlashCommandKind = + | "skill" + | "skills" + | "model" + | "new" + | "init" + | "resume" + | "continue" + | "mcp" + | "raw" + | "exit"; export type SlashCommandItem = { kind: SlashCommandKind; @@ -8,6 +18,7 @@ export type SlashCommandItem = { label: string; description: string; skill?: SkillInfo; + args?: string[]; }; export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [ @@ -53,6 +64,13 @@ export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [ label: "/mcp", description: "Show MCP server status and available tools", }, + { + kind: "raw", + name: "raw", + label: "/raw", + args: ["lite", "normal", "raw-scrollback"], + description: "Toggle display mode for viewing or collapsing reasoning content", + }, { kind: "exit", name: "exit", @@ -88,7 +106,7 @@ export function findExactSlashCommand(items: SlashCommandItem[], token: string): return null; } const query = token.slice(1); - const matches = items.filter((item) => item.name === query); + const matches = items.filter((item) => item.name.includes(query)); return matches.find((item) => item.kind !== "skill") ?? matches[0] ?? null; } From 67e2066b73f7e2d1b172e1e053d44009bcb7be0a Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 18 May 2026 20:20:26 +0800 Subject: [PATCH 005/212] =?UTF-8?q?feat(MessageView):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=8A=B6=E6=80=81=E8=A1=8C=E7=9A=84=20Plan?= =?UTF-8?q?=20Message=20=E9=A2=84=E8=A7=88=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 提取状态行文本为 statusLine 变量 - 创建 ToolSummary 对象汇总工具信息 - 获取并渲染更新计划的预览行 - 当有计划内容时,追加显示计划标题和内容 - 保持无计划时返回单行状态信息 --- src/ui/compoments/MessageView/utils.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/ui/compoments/MessageView/utils.ts b/src/ui/compoments/MessageView/utils.ts index a6ca4f64..45eb79c5 100644 --- a/src/ui/compoments/MessageView/utils.ts +++ b/src/ui/compoments/MessageView/utils.ts @@ -234,7 +234,21 @@ export function renderMessageToStdout(message: SessionMessage, mode: RawMode): s const name = payload.name || metaFunctionName || "tool"; const metaParams = typeof message.meta?.paramsMd === "string" ? message.meta.paramsMd.trim() : ""; const params = name.toLowerCase() === "bash" ? metaParams : truncate(metaParams, 120); - return `${chalk("✧")} ${chalk(formatStatusName(name))}${params ? ` ${chalk(params)}` : ""}`; + const statusLine = `${chalk("✧")} ${chalk(formatStatusName(name))}${params ? ` ${chalk(params)}` : ""}`; + + const summary: ToolSummary = { + name, + params, + ok: payload.ok !== false, + metadata: payload.metadata, + }; + const planLines = getUpdatePlanPreviewLines(summary); + if (planLines.length > 0) { + const planText = planLines.map((line) => ` ${line}`).join("\n"); + return `${statusLine}\n${chalk.dim(" └ Plan")}\n${planText}`; + } + + return statusLine; } if (message.role === "system") { From a42d5de1c1c6fbf935e53d0f470cf17c15361d63 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 19 May 2026 08:52:57 +0800 Subject: [PATCH 006/212] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8DMessageVie?= =?UTF-8?q?w=E7=BB=84=E4=BB=B6=E4=B8=ADStatusLine=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改StatusLine组件的params传值逻辑 - 当content存在时,params传入空字符串,避免显示错误 - 保持了内容渲染的兼容性和逻辑清晰性 --- src/ui/compoments/MessageView/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/compoments/MessageView/index.tsx b/src/ui/compoments/MessageView/index.tsx index cc0e4df5..dd0ddc56 100644 --- a/src/ui/compoments/MessageView/index.tsx +++ b/src/ui/compoments/MessageView/index.tsx @@ -50,7 +50,7 @@ export function MessageView({ message, collapsed, width = 80 }: MessageViewProps } return ( - + {content ? {renderMarkdown(content)} : null} From 05fed53801c402e78e64464847745ac7e959b119 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 19 May 2026 09:04:14 +0800 Subject: [PATCH 007/212] =?UTF-8?q?test(messageView):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=92=8C=E8=A7=A3=E6=9E=90=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E7=9A=84=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 renderMessageToStdout 函数的多场景测试,包括用户、助手、工具和系统消息的渲染行为 - 添加 getUpdatePlanPreviewLines 对 UpdatePlan 工具消息的计划内容提取测试 - 增加 parseToolPayload 函数对空内容、无效 JSON 和有效负载的解析测试 - 引入辅助函数 makeSessionMessage 以简化测试消息实例构造 - 确保各种边界条件和meta字段的渲染正确性验证 --- src/tests/messageView.test.ts | 178 +++++++++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 1 deletion(-) diff --git a/src/tests/messageView.test.ts b/src/tests/messageView.test.ts index 0981f916..0cd95dac 100644 --- a/src/tests/messageView.test.ts +++ b/src/tests/messageView.test.ts @@ -1,8 +1,15 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { parseDiffPreview } from "../ui"; -import { buildThinkingSummary } from "../ui/compoments/MessageView/utils"; +import { + buildThinkingSummary, + renderMessageToStdout, + getUpdatePlanPreviewLines, + parseToolPayload, +} from "../ui/compoments/MessageView/utils"; import { RawMode } from "../ui/contexts"; +import type { SessionMessage } from "../session"; +import type { ToolSummary } from "../ui/compoments/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { const lines = parseDiffPreview( @@ -52,3 +59,172 @@ test("MessageView shows full reasoning content in Normal/Raw mode", () => { "hidden chain of thought" ); }); + +// --- renderMessageToStdout tests --- + +function makeSessionMessage(overrides: Partial & Pick): SessionMessage { + const now = new Date().toISOString(); + return { + id: `test-${Math.random().toString(36).slice(2)}`, + sessionId: "test-session", + visible: true, + compacted: false, + createTime: now, + updateTime: now, + contentParams: null, + messageParams: null, + ...overrides, + }; +} + +test("renderMessageToStdout returns empty for invisible messages", () => { + const msg = makeSessionMessage({ role: "user", content: "hello", visible: false }); + assert.equal(renderMessageToStdout(msg, RawMode.Raw), ""); +}); + +test("renderMessageToStdout renders user messages with > prefix", () => { + const msg = makeSessionMessage({ role: "user", content: "fix the bug" }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("> fix the bug")); +}); + +test("renderMessageToStdout shows (no content) for empty user messages", () => { + const msg = makeSessionMessage({ role: "user", content: "" }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("(no content)")); +}); + +test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => { + const msg = makeSessionMessage({ role: "assistant", content: "Here is the fix" }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("✦")); + assert.ok(output.includes("Here is the fix")); +}); + +test("renderMessageToStdout renders assistant thinking messages with ✧ Thinking", () => { + const msg = makeSessionMessage({ + role: "assistant", + content: "Plan:\nAnalyze the code", + meta: { asThinking: true }, + }); + const output = renderMessageToStdout(msg, RawMode.Lite); + assert.ok(output.includes("✧")); + assert.ok(output.includes("Thinking")); + assert.ok(output.includes("Plan: Analyze the code")); +}); + +test("renderMessageToStdout renders tool messages with ✧ and tool name", () => { + const payload = JSON.stringify({ name: "read", ok: true }); + const msg = makeSessionMessage({ role: "tool", content: payload }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("✧")); + assert.ok(output.includes("Read")); +}); + +test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview", () => { + const payload = JSON.stringify({ + name: "UpdatePlan", + ok: true, + metadata: { plan: "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" }, + }); + const msg = makeSessionMessage({ role: "tool", content: payload }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("UpdatePlan")); + assert.ok(output.includes("└ Plan")); + assert.ok(output.includes("Step 1: Analyze")); + assert.ok(output.includes("Step 2: Implement")); +}); + +test("renderMessageToStdout renders system model change messages", () => { + const msg = makeSessionMessage({ + role: "system", + content: "Switched to deepseek-v4-pro", + meta: { isModelChange: true }, + }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("> Switched to deepseek-v4-pro")); +}); + +test("renderMessageToStdout renders system skill load messages", () => { + const msg = makeSessionMessage({ + role: "system", + content: "", + meta: { skill: { name: "code-review" } }, + }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("⚡ Loaded skill: code-review")); +}); + +test("renderMessageToStdout renders system summary messages", () => { + const msg = makeSessionMessage({ + role: "system", + content: "", + meta: { isSummary: true }, + }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("(conversation summary inserted)")); +}); + +test("renderMessageToStdout returns empty for unknown system messages", () => { + const msg = makeSessionMessage({ role: "system", content: "" }); + assert.equal(renderMessageToStdout(msg, RawMode.Raw), ""); +}); + +// --- getUpdatePlanPreviewLines tests --- + +test("getUpdatePlanPreviewLines returns empty for failed tool", () => { + const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: false, metadata: { plan: "Step 1" } }; + assert.deepEqual(getUpdatePlanPreviewLines(summary), []); +}); + +test("getUpdatePlanPreviewLines returns empty for non-UpdatePlan tool", () => { + const summary: ToolSummary = { name: "edit", params: "", ok: true, metadata: { plan: "Step 1" } }; + assert.deepEqual(getUpdatePlanPreviewLines(summary), []); +}); + +test("getUpdatePlanPreviewLines returns empty for missing plan metadata", () => { + const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: true, metadata: null }; + assert.deepEqual(getUpdatePlanPreviewLines(summary), []); +}); + +test("getUpdatePlanPreviewLines returns empty for empty plan string", () => { + const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: true, metadata: { plan: "" } }; + assert.deepEqual(getUpdatePlanPreviewLines(summary), []); +}); + +test("getUpdatePlanPreviewLines extracts plan lines and filters empty rows", () => { + const summary: ToolSummary = { + name: "UpdatePlan", + params: "", + ok: true, + metadata: { plan: "Step 1: Analyze\n\nStep 2: Implement\n \nStep 3: Test" }, + }; + assert.deepEqual(getUpdatePlanPreviewLines(summary), ["Step 1: Analyze", "Step 2: Implement", "Step 3: Test"]); +}); + +// --- parseToolPayload tests --- + +test("parseToolPayload returns defaults for null content", () => { + const result = parseToolPayload(null); + assert.deepEqual(result, { name: null, ok: true, metadata: null }); +}); + +test("parseToolPayload returns defaults for invalid JSON", () => { + const result = parseToolPayload("not valid json"); + assert.deepEqual(result, { name: null, ok: true, metadata: null }); +}); + +test("parseToolPayload parses valid JSON with name/ok/metadata", () => { + const result = parseToolPayload(JSON.stringify({ name: "read", ok: true, metadata: { file: "src/index.ts" } })); + assert.deepEqual(result, { name: "read", ok: true, metadata: { file: "src/index.ts" } }); +}); + +test("parseToolPayload respects ok: false", () => { + const result = parseToolPayload(JSON.stringify({ name: "bash", ok: false, metadata: null })); + assert.deepEqual(result, { name: "bash", ok: false, metadata: null }); +}); + +test("parseToolPayload trims whitespace from name", () => { + const result = parseToolPayload(JSON.stringify({ name: " read ", ok: true })); + assert.equal(result.name, "read"); +}); From 418294dfd315402e7942a42960239185fa44ef0a Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 19 May 2026 10:28:48 +0800 Subject: [PATCH 008/212] =?UTF-8?q?refactor(rawmode):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20RawMode=20=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E4=B8=8E?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=BA=A4=E4=BA=92=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 RawModeContext 中增加 previousMode 状态用于保存上一个模式 - 修改 setMode 逻辑以更新 previousMode,支持通过函数设置模式 - RawModeExitPrompt 捕获并使用快照 previousMode 作为退出时目标模式 - 调整 App.tsx 处理 RawMode 切换逻辑,避免界面闪烁并重置欢迎屏幕状态 - 处理无激活会话时显示欢迎屏幕,确保状态正确更新 - 优化 Raw 模式消息加载逻辑,避免活跃会话缺失时的错误 - 更新测试用例中消息构建函数支持更多可选属性与默认值设置 - 修改 renderMessageToStdout 测试示例以配合新的消息结构及元信息 --- src/tests/messageView.test.ts | 23 +++++++------ src/ui/App.tsx | 24 +++++++++----- src/ui/compoments/RawModeExitPrompt/index.tsx | 11 +++++-- src/ui/contexts/RawModeContext.tsx | 32 ++++++++++++++++--- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/tests/messageView.test.ts b/src/tests/messageView.test.ts index 0cd95dac..b97e1250 100644 --- a/src/tests/messageView.test.ts +++ b/src/tests/messageView.test.ts @@ -65,15 +65,18 @@ test("MessageView shows full reasoning content in Normal/Raw mode", () => { function makeSessionMessage(overrides: Partial & Pick): SessionMessage { const now = new Date().toISOString(); return { - id: `test-${Math.random().toString(36).slice(2)}`, - sessionId: "test-session", - visible: true, - compacted: false, - createTime: now, - updateTime: now, - contentParams: null, - messageParams: null, - ...overrides, + id: overrides.id ?? `test-${Math.random().toString(36).slice(2)}`, + sessionId: overrides.sessionId ?? "test-session", + role: overrides.role, + content: overrides.content ?? null, + visible: overrides.visible ?? true, + compacted: overrides.compacted ?? false, + createTime: overrides.createTime ?? now, + updateTime: overrides.updateTime ?? now, + contentParams: overrides.contentParams ?? null, + messageParams: overrides.messageParams ?? null, + meta: overrides.meta, + html: overrides.html, }; } @@ -149,7 +152,7 @@ test("renderMessageToStdout renders system skill load messages", () => { const msg = makeSessionMessage({ role: "system", content: "", - meta: { skill: { name: "code-review" } }, + meta: { skill: { name: "code-review", path: "", description: "" } }, }); const output = renderMessageToStdout(msg, RawMode.Raw); assert.ok(output.includes("⚡ Loaded skill: code-review")); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 3d29e32a..9189df6c 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -374,19 +374,18 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. const handleRawModeChange = useCallback( (nextMode: string) => { const activeSessionId = sessionManager.getActiveSessionId(); - if (!activeSessionId) { - return; - } - setMode(nextMode as RawMode); - + // Reset chat view state synchronously so the transition frame does not + // re-render a stale welcome screen before handleSelectSession runs. + setShowWelcome(false); + setMessages([]); // Clear screen to remove stale formatted text. process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); setTimeout(() => { if (nextMode === RawMode.Raw) { // Write all messages directly to stdout for raw scrollback mode. - const allMessages = loadVisibleMessages(sessionManager, activeSessionId); + const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : []; for (const msg of allMessages) { process.stdout.write("\n"); process.stdout.write(renderMessageToStdout(msg, nextMode) + "\n\n"); @@ -394,10 +393,19 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. if (allMessages.length > 0) { process.stdout.write("\n\n"); process.stdout.write(chalk.dim("Press ESC to exit raw mode")); + } else { + process.stdout.write("\n"); + process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)")); + process.stdout.write("\n\n"); + process.stdout.write(chalk.dim("Press ESC to exit raw mode")); } - } else { + } else if (activeSessionId) { // Switch to chat view to render messages. handleSelectSession(activeSessionId); + } else { + // No active session: just show the welcome screen once. + setWelcomeNonce((n) => n + 1); + setShowWelcome(true); } }, 200); }, @@ -499,7 +507,7 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. }, [pendingQuestion]); if (mode === RawMode.Raw) { - return handleRawModeChange(RawMode.None)} />; + return handleRawModeChange(prev)} />; } return ( diff --git a/src/ui/compoments/RawModeExitPrompt/index.tsx b/src/ui/compoments/RawModeExitPrompt/index.tsx index 9b1d218b..57ebf074 100644 --- a/src/ui/compoments/RawModeExitPrompt/index.tsx +++ b/src/ui/compoments/RawModeExitPrompt/index.tsx @@ -1,11 +1,16 @@ -import type React from "react"; +import { useRef, type ReactElement } from "react"; import { useInput } from "ink"; +import { useRawModeContext, type RawMode } from "../../contexts"; + +export function RawModeExitPrompt({ onExit }: { onExit: (previousMode: RawMode) => void }): ReactElement | null { + const { previousMode } = useRawModeContext(); + // Snapshot the prior mode at mount so later context updates do not change the ESC target. + const snapshotRef = useRef(previousMode); -export function RawModeExitPrompt({ onExit }: { onExit: () => void }): React.ReactElement | null { useInput( (_input, key) => { if (key.escape) { - onExit(); + onExit(snapshotRef.current); } }, { isActive: true } diff --git a/src/ui/contexts/RawModeContext.tsx b/src/ui/contexts/RawModeContext.tsx index d0c23368..6fbc7063 100644 --- a/src/ui/contexts/RawModeContext.tsx +++ b/src/ui/contexts/RawModeContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useState } from "react"; +import React, { createContext, useCallback, useContext, useRef, useState } from "react"; import type { DropdownMenuItem } from "../DropdownMenu"; export enum RawMode { @@ -21,9 +21,17 @@ export const RAW_COMMAND_MODELS: DropdownMenuItem[] = [ }, ] as const; -const RawModeContext = createContext<{ mode: RawMode; setMode: React.Dispatch> }>({ +type RawModeContextValue = { + mode: RawMode; + setMode: React.Dispatch>; + // The mode that was active right before the most recent mode transition. + previousMode: RawMode; +}; + +const RawModeContext = createContext({ mode: RawMode.Lite, setMode: () => {}, + previousMode: RawMode.Lite, }); export function useRawModeContext() { @@ -35,6 +43,22 @@ export function useRawModeContext() { } export const RawModeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [mode, setMode] = useState(RawMode.Lite); - return {children}; + const [mode, _setMode] = useState(RawMode.Lite); + const previousModeRef = useRef(RawMode.Lite); + + const setMode = useCallback>>((next) => { + _setMode((current) => { + const resolved = typeof next === "function" ? (next as (prev: RawMode) => RawMode)(current) : next; + if (resolved !== current) { + previousModeRef.current = current; + } + return resolved; + }); + }, []); + + return ( + + {children} + + ); }; From 5bfce46d6d17ea88e7f02b56b5f7282b37f9ebd9 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 19 May 2026 11:11:40 +0800 Subject: [PATCH 009/212] =?UTF-8?q?docs(readme):=20=E5=A2=9E=E5=BC=BA=20RE?= =?UTF-8?q?ADME.md=20=E6=96=87=E4=BB=B6=E5=86=85=E5=AE=B9=E5=92=8C?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 README.md 和 README_en.md 文件中新增居中标题和统一头部样式 - README.md 中添加“[English](./README_en.md) · 中文”语言切换链接 - README_en.md 中添加“English · [中文](./README.md)”语言切换链接 - README.md 新增 `/raw` 命令介绍,补充命令表内容 - 删除冗余的 README_cn.md 文件,简化文档管理 --- README.md | 16 +++++- README_cn.md | 154 --------------------------------------------------- README_en.md | 17 +++++- 3 files changed, 31 insertions(+), 156 deletions(-) delete mode 100644 README_cn.md diff --git a/README.md b/README.md index 69d28c82..00167c1c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,17 @@ -# Deep Code CLI +
+
+
+

+ + deepcode-cli + +

+

Deep Code CLI

+ +[English](./README_en.md) · 中文 + +
+
[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制、Agent Skills 以及 MCP 集成。 @@ -53,6 +66,7 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力: | `/new` | 开始新对话 | | `/resume` | 选择历史对话继续 | | `/model` | 切换模型、思考模式和推理强度 | +| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)| | `/init` | 初始化 AGENTS.md 文件 | | `/skills` | 列出可用 skills | | `/mcp` | 查看 MCP 服务器状态和可用工具 | diff --git a/README_cn.md b/README_cn.md deleted file mode 100644 index 69d28c82..00000000 --- a/README_cn.md +++ /dev/null @@ -1,154 +0,0 @@ -# Deep Code CLI - -[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制、Agent Skills 以及 MCP 集成。 - -## 安装 - -```bash -npm install -g @vegamo/deepcode-cli -``` - -在任意项目目录下运行 `deepcode` 即可启动。 - -![intro2](resources/intro2.png) - -## 配置 - -创建 `~/.deepcode/settings.json` 文件,内容如下: - -```json -{ - "env": { - "MODEL": "deepseek-v4-pro", - "BASE_URL": "https://api.deepseek.com", - "API_KEY": "sk-..." - }, - "thinkingEnabled": true, - "reasoningEffort": "max" -} -``` - -配置文件与 [Deep Code VSCode 插件](https://github.com/lessweb/deepcode) 共享,无需重复配置。 - -完整配置说明(多层级优先级、环境变量等)请参阅 [docs/configuration.md](docs/configuration.md)。 - -## 主要功能 - -### **Skills** -Deep Code CLI 支持 agent skills,允许您扩展助手的能力: - -- **User-level Skills**:从 `~/.agents/skills/` 目录中发现并激活 skills。 -- **Project-level Skills**:从 `./.agents/skills/` 目录中加载项目专属 skills,并兼容旧的 `./.deepcode/skills/` 目录。 - -### **为 DeepSeek 优化** -- 专门为 DeepSeek 模型性能调优。 -- 通过使用[上下文缓存](https://api-docs.deepseek.com/guides/kv_cache)来降低成本。 -- 原生支持[思考模式](https://api-docs.deepseek.com/guides/thinking_mode)和思考强度控制。 - -## 斜杠命令与按键功能 - -| 斜杠命令 | 操作 | -|-----------------|---------------------------------------------| -| `/` | 打开 skills / 命令菜单 | -| `/new` | 开始新对话 | -| `/resume` | 选择历史对话继续 | -| `/model` | 切换模型、思考模式和推理强度 | -| `/init` | 初始化 AGENTS.md 文件 | -| `/skills` | 列出可用 skills | -| `/mcp` | 查看 MCP 服务器状态和可用工具 | -| `/exit` | 退出(也可用连续 `Ctrl+D`) | - -| 按键 | 操作 | -|-----------------|---------------------------------------------| -| `Enter` | 发送消息 | -| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | -| `Ctrl+V` | 从剪贴板粘贴图片 | -| `Esc` | 中断当前模型回复 | -| 连续 `Ctrl+D` | 退出 | - -## 支持的模型 - -- `deepseek-v4-pro`(推荐使用) -- `deepseek-v4-flash` -- 任何其他 OpenAI 兼容模型 - - -## 常见问题 - -### Deep Code 是否有 VSCode 插件? - -有的。Deep Code 提供功能完整的 VSCode 插件,可在 [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=vegamo.deepcode-vscode) 安装。插件与 CLI 共享 `~/.deepcode/settings.json` 配置文件,可以在终端和编辑器之间无缝切换。 - -### Deep Code 是否支持理解图片? - -Deep Code 支持多模态,可使用ctrl+v从剪贴板粘贴图片。但目前 deepseek-v4 不支持多模态。有些模型虽然有多模态能力,但对多轮对话请求的限制太严。目前多模态输入推荐使用火山方舟的 Doubao-Seed-2.0-pro 模型,适配效果最好。 - -### 怎样在任务完成后自动给 Slack 发消息? - -编写一个调用 Slack webhook 的 Shell 通知脚本,然后在 `~/.deepcode/settings.json` 中将 `notify` 字段设为该脚本的完整路径即可。详细步骤可参考:https://binfer.net/share/jby5xnc-so6g - -### 怎样启用联网搜索功能? - -Deep Code自带免费的、且大部分情况够用的Web Search工具。如果你希望使用自定义脚本进行联网搜索,可以在 `~/.deepcode/settings.json` 中将 `webSearchTool` 设为脚本的完整路径即可。详细步骤可参考:https://github.com/qorzj/web_search_cli - -### 如何配置 MCP? - -Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览器、数据库等外部服务。在 `settings.json` 中配置 `mcpServers` 字段即可启用,启动后使用 `/mcp` 命令查看已配置的 MCP 服务器状态和可用工具。 - -详细配置指南:[docs/mcp.md](docs/mcp.md) - - -### 是否支持 Coding Plan? - -支持。只要把 `~/.deepcode/settings.json` 的 `env.BASE_URL` 配置为 OpenAI 兼容的接口地址就行。以火山方舟的 Coding Plan 为例: - -```json -{ - "env": { - "MODEL": "ark-code-latest", - "BASE_URL": "https://ark.cn-beijing.volces.com/api/coding/v3", - "API_KEY": "**************" - }, - "thinkingEnabled": true -} -``` -## 贡献 - -欢迎贡献代码!以下是参与方式: - -```bash -# 克隆仓库 -git clone https://github.com/lessweb/deepcode-cli.git -cd deepcode-cli - -# 安装依赖 -npm install - -# 本地开发(类型检查 + lint + 格式检查 + 构建) -npm run build - -# 运行测试 -npm test - -# 链接到全局(即本地全局安装) -npm link -``` - -- 提交 PR 前请确保 `npm run check` 通过(类型检查 + lint + 格式检查) -- 建议在执行构建前,先执行 `npm run format` 自动格式化代码,避免构建报错 - -## 获取帮助 - -- 在 GitHub Issues 上报告错误或请求功能 (https://github.com/lessweb/deepcode-cli/issues) - -## 协议 - -- MIT - -## 支持我们 - -如果你觉得这个工具对你有帮助,请考虑通过以下方式支持我们: - -- 在 GitHub 上给我们一个 Star (https://github.com/lessweb/deepcode-cli) -- 向我们提交反馈和建议 -- 分享给你的朋友和同事 diff --git a/README_en.md b/README_en.md index ee5a1034..4c78cbd2 100644 --- a/README_en.md +++ b/README_en.md @@ -1,7 +1,21 @@ -# Deep Code CLI +
+
+
+

+ + deepcode-cli + +

+

Deep Code CLI

+ +English · [中文](./README.md) + +
+
[Deep Code](https://github.com/lessweb/deepcode-cli) is a terminal AI coding assistant optimized for the `deepseek-v4` model, with support for deep thinking, reasoning effort control, Agent Skills, and MCP (Model Context Protocol) integration. + ## Installation ```bash @@ -53,6 +67,7 @@ Deep Code CLI supports agent skills that allow you to extend the assistant's cap | `/new` | Start a fresh conversation | | `/resume` | Choose a previous conversation to continue | | `/model` | Switch model, thinking mode, and reasoning effort | +| `/raw` | Toggle display mode (Normal / Lite / Raw scrollback) | | `/init` | Initialize an AGENTS.md file (LLM project instructions) | | `/skills` | List available skills | | `/mcp` | View MCP server status and available tools | From 7f3d1b87dfb2a79b95d94289ef558d3b04c40e40 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 19 May 2026 13:17:54 +0800 Subject: [PATCH 010/212] =?UTF-8?q?fix(ui):=20=E4=BC=98=E5=8C=96=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E9=92=A9=E5=AD=90=E4=B8=8E=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=A1=8C=E5=AF=BC=E5=85=A5=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 useAppContext 钩子,安全处理无上下文情况,返回默认版本信息 - 更新 cli.tsx 中 AppContainer 的导入方式,改为从统一入口导入 - 在 ui/index.ts 中导出 AppContainer 组件 - 新增 UI 共享常量 ARGS_SEPARATOR,提升分隔符一致性 feat(ui): 优化命令行提示参数分隔符显示 - 在 PromptInput 和 SlashCommandMenu 组件中使用 ARGS_SEPARATOR 替代硬编码分隔符 - 调整 SlashCommandMenu 中命令行长度计算逻辑,兼容新分隔符 fix(ui): 修正 slashCommands 过滤匹配逻辑 - 将命令匹配条件从包含改为完全相等,提高准确性 feat(ui): 扩展消息视图工具信息展示 - 增加对 tool 消息中 meta.resultMd 字段的渲染支持 - 在工具状态行后增加 Result 结果块,配合 Plan 预览一同展示 - 更新 renderMessageToStdout 相关测试,覆盖新展示逻辑 --- src/cli.tsx | 2 +- src/tests/messageView.test.ts | 36 ++++++++++++++++++++++++++ src/ui/PromptInput.tsx | 3 ++- src/ui/SlashCommandMenu.tsx | 7 +++-- src/ui/compoments/MessageView/utils.ts | 7 +++-- src/ui/constants.ts | 4 +++ src/ui/contexts/AppContext.tsx | 5 ++-- src/ui/index.ts | 2 +- src/ui/slashCommands.ts | 2 +- 9 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 src/ui/constants.ts diff --git a/src/cli.tsx b/src/cli.tsx index e8e8659e..66ceb7d8 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -2,7 +2,7 @@ import React from "react"; import { render } from "ink"; import { setShellIfWindows } from "./common/shell-utils"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./updateCheck"; -import AppContainer from "./ui/AppContainer"; +import { AppContainer } from "./ui"; const args = process.argv.slice(2); const packageInfo = readPackageInfo(); diff --git a/src/tests/messageView.test.ts b/src/tests/messageView.test.ts index b97e1250..990c8ff7 100644 --- a/src/tests/messageView.test.ts +++ b/src/tests/messageView.test.ts @@ -124,6 +124,40 @@ test("renderMessageToStdout renders tool messages with ✧ and tool name", () => assert.ok(output.includes("Read")); }); +test("renderMessageToStdout renders tool messages with resultMd output", () => { + const payload = JSON.stringify({ name: "read", ok: true }); + const msg = makeSessionMessage({ + role: "tool", + content: payload, + meta: { resultMd: "File content:\n line 1\n line 2" }, + }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("✧")); + assert.ok(output.includes("Read")); + assert.ok(output.includes("└ Result")); + assert.ok(output.includes("File content:")); + assert.ok(output.includes("line 1")); +}); + +test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview and resultMd", () => { + const payload = JSON.stringify({ + name: "UpdatePlan", + ok: true, + metadata: { plan: "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" }, + }); + const msg = makeSessionMessage({ + role: "tool", + content: payload, + meta: { resultMd: "Plan updated successfully" }, + }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes("UpdatePlan")); + assert.ok(output.includes("└ Plan")); + assert.ok(output.includes("Step 1: Analyze")); + assert.ok(output.includes(" Result")); + assert.ok(output.includes("Plan updated successfully")); +}); + test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview", () => { const payload = JSON.stringify({ name: "UpdatePlan", @@ -136,6 +170,8 @@ test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview", assert.ok(output.includes("└ Plan")); assert.ok(output.includes("Step 1: Analyze")); assert.ok(output.includes("Step 2: Implement")); + // Verify resultMd is NOT included when meta.resultMd is absent + assert.ok(!output.includes("└ Result")); }); test("renderMessageToStdout renders system model change messages", () => { diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index d620d24b..1096a936 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text, useApp, useStdout } from "ink"; import chalk from "chalk"; +import { ARGS_SEPARATOR } from "./constants"; import { EMPTY_BUFFER, backspace, @@ -887,7 +888,7 @@ export const PromptInput = React.memo(function PromptInput({ ); const matchedCommand = slashToken ? findExactSlashCommand(slashItems, slashToken) : null; - const inlineHint = matchedCommand?.args ? ` ${matchedCommand.args.join("|")}` : ""; + const inlineHint = matchedCommand?.args ? ` ${matchedCommand.args.join(ARGS_SEPARATOR)}` : ""; return ( diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/SlashCommandMenu.tsx index 1c050b94..02ff3084 100644 --- a/src/ui/SlashCommandMenu.tsx +++ b/src/ui/SlashCommandMenu.tsx @@ -1,5 +1,6 @@ import { formatSlashCommandDescription, formatSlashCommandLabel } from "./slashCommands"; import type { SlashCommandItem } from "./slashCommands"; +import { ARGS_SEPARATOR } from "./constants"; import React from "react"; import { Box, Text } from "ink"; @@ -21,7 +22,9 @@ const SlashCommandMenu = React.memo(function SlashCommandMenu({ if (items.length === 0) { return 0; } - const longestLabel = Math.max(...items.map((s) => s.label.length + (s.args ? s.args?.join("|")?.length + 4 : 0))); + const longestLabel = Math.max( + ...items.map((s) => s.label.length + (s.args ? s.args?.join(ARGS_SEPARATOR)?.length + 4 : 0)) + ); const contentWidth = longestLabel + 2; // +2 for prefix "> " or " " const maxAllowed = Math.max(10, (width - 2) >> 1); // 容器50%宽度(减去gap),至少保留10列 return Math.min(contentWidth, maxAllowed); @@ -54,7 +57,7 @@ const SlashCommandMenu = React.memo(function SlashCommandMenu({ {actualIndex === activeIndex ? "> " : " "} {formatSlashCommandLabel(item)}
- {item.args ? {item.args.join("|")} : null} + {item.args ? {item.args.join(ARGS_SEPARATOR)} : null}
diff --git a/src/ui/compoments/MessageView/utils.ts b/src/ui/compoments/MessageView/utils.ts index 45eb79c5..af5391d8 100644 --- a/src/ui/compoments/MessageView/utils.ts +++ b/src/ui/compoments/MessageView/utils.ts @@ -236,6 +236,9 @@ export function renderMessageToStdout(message: SessionMessage, mode: RawMode): s const params = name.toLowerCase() === "bash" ? metaParams : truncate(metaParams, 120); const statusLine = `${chalk("✧")} ${chalk(formatStatusName(name))}${params ? ` ${chalk(params)}` : ""}`; + const metaResultMd = typeof message.meta?.resultMd === "string" ? message.meta.resultMd.trim() : ""; + const result = metaResultMd ? `\n${chalk.dim(" └ Result")}\n${metaResultMd}` : ""; + const summary: ToolSummary = { name, params, @@ -245,10 +248,10 @@ export function renderMessageToStdout(message: SessionMessage, mode: RawMode): s const planLines = getUpdatePlanPreviewLines(summary); if (planLines.length > 0) { const planText = planLines.map((line) => ` ${line}`).join("\n"); - return `${statusLine}\n${chalk.dim(" └ Plan")}\n${planText}`; + return `${statusLine}\n${chalk.dim(" └ Plan")}\n${planText}${result}`; } - return statusLine; + return `${statusLine}${result}`; } if (message.role === "system") { diff --git a/src/ui/constants.ts b/src/ui/constants.ts new file mode 100644 index 00000000..7c74597b --- /dev/null +++ b/src/ui/constants.ts @@ -0,0 +1,4 @@ +// UI-level shared constants used across components. + +/** Separator used when rendering command arguments inline (e.g., `arg1 | arg2 | arg3`). */ +export const ARGS_SEPARATOR = " | "; diff --git a/src/ui/contexts/AppContext.tsx b/src/ui/contexts/AppContext.tsx index 34d45894..41b1d1d4 100644 --- a/src/ui/contexts/AppContext.tsx +++ b/src/ui/contexts/AppContext.tsx @@ -6,10 +6,11 @@ export interface AppState { export const AppContext = createContext(null); -export const useAppContext = () => { +export const useAppContext = (): AppState => { const context = useContext(AppContext); if (!context) { - throw new Error("useAppContext must be used within an AppProvider"); + // Safe fallback when App is rendered without AppContainer (e.g., in tests). + return { version: "unknown" }; } return context; }; diff --git a/src/ui/index.ts b/src/ui/index.ts index 3a1ddf46..f2e698c1 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -1,5 +1,4 @@ export { - App, readSettings, readProjectSettings, writeSettings, @@ -8,6 +7,7 @@ export { resolveCurrentSettings, createOpenAIClient, } from "./App"; +export { default as AppContainer } from "./AppContainer"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; export { MessageView } from "./compoments"; export { parseDiffPreview } from "./compoments/MessageView/utils"; diff --git a/src/ui/slashCommands.ts b/src/ui/slashCommands.ts index aab06bd6..948a7abd 100644 --- a/src/ui/slashCommands.ts +++ b/src/ui/slashCommands.ts @@ -106,7 +106,7 @@ export function findExactSlashCommand(items: SlashCommandItem[], token: string): return null; } const query = token.slice(1); - const matches = items.filter((item) => item.name.includes(query)); + const matches = items.filter((item) => item.name === query); return matches.find((item) => item.kind !== "skill") ?? matches[0] ?? null; } From 379ffc5b45f9973e5738718657411f87c6db26cf Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 19 May 2026 14:30:49 +0800 Subject: [PATCH 011/212] =?UTF-8?q?docs(readme):=20=E6=81=A2=E5=A4=8D=20RE?= =?UTF-8?q?ADME-zh=5FCN.md=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README_en.md => README-en.md | 0 README-zh_CN.md | 168 +++++++++++++++++++++++++++++++++++ README.md | 2 +- 3 files changed, 169 insertions(+), 1 deletion(-) rename README_en.md => README-en.md (100%) create mode 100644 README-zh_CN.md diff --git a/README_en.md b/README-en.md similarity index 100% rename from README_en.md rename to README-en.md diff --git a/README-zh_CN.md b/README-zh_CN.md new file mode 100644 index 00000000..98346b65 --- /dev/null +++ b/README-zh_CN.md @@ -0,0 +1,168 @@ +
+
+
+

+ + deepcode-cli + +

+

Deep Code CLI

+ +[English](README-en.md) · 中文 + +
+
+ +[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制、Agent Skills 以及 MCP 集成。 + +## 安装 + +```bash +npm install -g @vegamo/deepcode-cli +``` + +在任意项目目录下运行 `deepcode` 即可启动。 + +![intro2](resources/intro2.png) + +## 配置 + +创建 `~/.deepcode/settings.json` 文件,内容如下: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max" +} +``` + +配置文件与 [Deep Code VSCode 插件](https://github.com/lessweb/deepcode) 共享,无需重复配置。 + +完整配置说明(多层级优先级、环境变量等)请参阅 [docs/configuration.md](docs/configuration.md)。 + +## 主要功能 + +### **Skills** +Deep Code CLI 支持 agent skills,允许您扩展助手的能力: + +- **User-level Skills**:从 `~/.agents/skills/` 目录中发现并激活 skills。 +- **Project-level Skills**:从 `./.agents/skills/` 目录中加载项目专属 skills,并兼容旧的 `./.deepcode/skills/` 目录。 + +### **为 DeepSeek 优化** +- 专门为 DeepSeek 模型性能调优。 +- 通过使用[上下文缓存](https://api-docs.deepseek.com/guides/kv_cache)来降低成本。 +- 原生支持[思考模式](https://api-docs.deepseek.com/guides/thinking_mode)和思考强度控制。 + +## 斜杠命令与按键功能 + +| 斜杠命令 | 操作 | +|-----------------|---------------------------------------------| +| `/` | 打开 skills / 命令菜单 | +| `/new` | 开始新对话 | +| `/resume` | 选择历史对话继续 | +| `/model` | 切换模型、思考模式和推理强度 | +| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)| +| `/init` | 初始化 AGENTS.md 文件 | +| `/skills` | 列出可用 skills | +| `/mcp` | 查看 MCP 服务器状态和可用工具 | +| `/exit` | 退出(也可用连续 `Ctrl+D`) | + +| 按键 | 操作 | +|-----------------|---------------------------------------------| +| `Enter` | 发送消息 | +| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | +| `Ctrl+V` | 从剪贴板粘贴图片 | +| `Esc` | 中断当前模型回复 | +| 连续 `Ctrl+D` | 退出 | + +## 支持的模型 + +- `deepseek-v4-pro`(推荐使用) +- `deepseek-v4-flash` +- 任何其他 OpenAI 兼容模型 + + +## 常见问题 + +### Deep Code 是否有 VSCode 插件? + +有的。Deep Code 提供功能完整的 VSCode 插件,可在 [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=vegamo.deepcode-vscode) 安装。插件与 CLI 共享 `~/.deepcode/settings.json` 配置文件,可以在终端和编辑器之间无缝切换。 + +### Deep Code 是否支持理解图片? + +Deep Code 支持多模态,可使用ctrl+v从剪贴板粘贴图片。但目前 deepseek-v4 不支持多模态。有些模型虽然有多模态能力,但对多轮对话请求的限制太严。目前多模态输入推荐使用火山方舟的 Doubao-Seed-2.0-pro 模型,适配效果最好。 + +### 怎样在任务完成后自动给 Slack 发消息? + +编写一个调用 Slack webhook 的 Shell 通知脚本,然后在 `~/.deepcode/settings.json` 中将 `notify` 字段设为该脚本的完整路径即可。详细步骤可参考:https://binfer.net/share/jby5xnc-so6g + +### 怎样启用联网搜索功能? + +Deep Code自带免费的、且大部分情况够用的Web Search工具。如果你希望使用自定义脚本进行联网搜索,可以在 `~/.deepcode/settings.json` 中将 `webSearchTool` 设为脚本的完整路径即可。详细步骤可参考:https://github.com/qorzj/web_search_cli + +### 如何配置 MCP? + +Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览器、数据库等外部服务。在 `settings.json` 中配置 `mcpServers` 字段即可启用,启动后使用 `/mcp` 命令查看已配置的 MCP 服务器状态和可用工具。 + +详细配置指南:[docs/mcp.md](docs/mcp.md) + + +### 是否支持 Coding Plan? + +支持。只要把 `~/.deepcode/settings.json` 的 `env.BASE_URL` 配置为 OpenAI 兼容的接口地址就行。以火山方舟的 Coding Plan 为例: + +```json +{ + "env": { + "MODEL": "ark-code-latest", + "BASE_URL": "https://ark.cn-beijing.volces.com/api/coding/v3", + "API_KEY": "**************" + }, + "thinkingEnabled": true +} +``` +## 贡献 + +欢迎贡献代码!以下是参与方式: + +```bash +# 克隆仓库 +git clone https://github.com/lessweb/deepcode-cli.git +cd deepcode-cli + +# 安装依赖 +npm install + +# 本地开发(类型检查 + lint + 格式检查 + 构建) +npm run build + +# 运行测试 +npm test + +# 链接到全局(即本地全局安装) +npm link +``` + +- 提交 PR 前请确保 `npm run check` 通过(类型检查 + lint + 格式检查) +- 建议在执行构建前,先执行 `npm run format` 自动格式化代码,避免构建报错 + +## 获取帮助 + +- 在 GitHub Issues 上报告错误或请求功能 (https://github.com/lessweb/deepcode-cli/issues) + +## 协议 + +- MIT + +## 支持我们 + +如果你觉得这个工具对你有帮助,请考虑通过以下方式支持我们: + +- 在 GitHub 上给我们一个 Star (https://github.com/lessweb/deepcode-cli) +- 向我们提交反馈和建议 +- 分享给你的朋友和同事 diff --git a/README.md b/README.md index 00167c1c..98346b65 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

Deep Code CLI

-[English](./README_en.md) · 中文 +[English](README-en.md) · 中文
From 3fef0fc5137af49f218237fa0e919159fb231122 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 10:09:38 +0800 Subject: [PATCH 012/212] feat(notify): pass STATUS, FAIL_REASON, BODY as env vars to notify hook - Add NotifyContext type with status, failReason, body fields - buildNotifyEnv injects STATUS, FAIL_REASON, BODY when provided - maybeNotifyTaskCompletion extracts last assistant message as BODY - launchNotifyScript accepts optional context parameter - Add unit tests for new context env var injection - Update docs with env variable table and iTerm2/macOS notify examples --- docs/configuration.md | 34 ++++++ docs/configuration_en.md | 34 ++++++ src/common/notify.ts | 38 ++++++- src/session.ts | 18 +++- src/tests/session.test.ts | 144 ++++++++++++++++++++++++++ src/tests/settings-and-notify.test.ts | 65 +++++++++++- 6 files changed, 324 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f8e52c3a..45aaab06 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -67,12 +67,46 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 设置一个 Shell 脚本的完整路径。当 AI 助手完成一轮任务后,会自动执行该脚本,可用于发送通知(如 Slack 消息)。 +通知脚本执行时,会通过环境变量注入以下上下文信息: + +| 环境变量 | 说明 | +|----------|------| +| `DURATION` | 会话耗时,单位秒(整数) | +| `STATUS` | 会话状态:`"completed"` 或 `"failed"` | +| `FAIL_REASON` | 失败原因(仅失败时设置) | +| `BODY` | 最后一条 AI 助手回复的文本内容 | +| `TITLE` | 会话标题(对应 resume 列表中的标题) | + ```json { "notify": "/path/to/slack-notify.sh" } ``` +**iTerm2 终端通知示例**: + +如果你的终端是 iTerm2,可以直接通过 OSC 9 转义序列弹出通知,无需额外脚本。创建以下脚本(如 `~/.deepcode/notify.sh`): + +```bash +#!/bin/bash +# iTerm2 OSC 9 通知 +echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +**macOS 系统通知示例**: + +```bash +#!/bin/bash +# macOS 系统通知 +osascript -e "display notification \"任务已${STATUS:-完成},耗时 ${DURATION}s\" with title \"DeepCode\"" +``` + #### `webSearchTool` — 自定义联网搜索 Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索逻辑,可将 `webSearchTool` 设为一个可执行脚本的完整路径: diff --git a/docs/configuration_en.md b/docs/configuration_en.md index 369f8e47..606fcab9 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -67,12 +67,46 @@ When thinking mode is enabled, controls the depth of the model’s reasoning: Set a full path to a shell script. When the AI assistant finishes a round of tasks, the script is executed automatically, which can be used to send notifications (e.g., a Slack message). +The following context is injected as environment variables when the notify script runs: + +| Variable | Description | +|----------|-------------| +| `DURATION` | Session duration in seconds (integer) | +| `STATUS` | Session status: `"completed"` or `"failed"` | +| `FAIL_REASON` | Failure reason (only set on failure) | +| `BODY` | The text content of the last AI assistant reply | +| `TITLE` | Session title (matches the resume list title) | + ```json { "notify": "/path/to/slack-notify.sh" } ``` +**iTerm2 Notification Example**: + +On iTerm2 you can use the OSC 9 escape sequence for native notifications. Create a script (e.g., `~/.deepcode/notify.sh`): + +```bash +#!/bin/bash +# iTerm2 OSC 9 notification +echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +**macOS System Notification Example**: + +```bash +#!/bin/bash +# macOS system notification +osascript -e "display notification \"Task ${STATUS:-completed}, took ${DURATION}s\" with title \"DeepCode\"" +``` + #### `webSearchTool` — Custom Web Search Deep Code has a built-in, free-to-use Web Search tool. If you need custom search logic, set `webSearchTool` to the full path of an executable script: diff --git a/src/common/notify.ts b/src/common/notify.ts index 8878c508..d1b541b5 100644 --- a/src/common/notify.ts +++ b/src/common/notify.ts @@ -16,11 +16,40 @@ export function formatDurationSeconds(durationMs: number): string { return String(Math.floor(safeMs / 1000)); } -export function buildNotifyEnv(durationMs: number, baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { - return { +export type NotifyContext = { + status?: string; + failReason?: string; + body?: string; + title?: string; +}; + +export function buildNotifyEnv( + durationMs: number, + baseEnv: NodeJS.ProcessEnv = process.env, + context: NotifyContext = {} +): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { ...baseEnv, DURATION: formatDurationSeconds(durationMs), }; + delete env.STATUS; + delete env.FAIL_REASON; + delete env.BODY; + delete env.TITLE; + + if (context.status) { + env.STATUS = context.status; + } + if (context.failReason) { + env.FAIL_REASON = context.failReason; + } + if (context.body) { + env.BODY = context.body; + } + if (context.title) { + env.TITLE = context.title; + } + return env; } export function launchNotifyScript( @@ -28,7 +57,8 @@ export function launchNotifyScript( durationMs: number, workingDirectory?: string, spawnProcess: NotifySpawn = spawn as unknown as NotifySpawn, - configuredEnv: Record = {} + configuredEnv: Record = {}, + context: NotifyContext = {} ): void { const commandPath = notifyPath?.trim(); if (!commandPath) { @@ -38,7 +68,7 @@ export function launchNotifyScript( const options = { cwd: workingDirectory, detached: process.platform !== "win32", - env: buildNotifyEnv(durationMs, { ...process.env, ...configuredEnv }), + env: buildNotifyEnv(durationMs, { ...process.env, ...configuredEnv }, context), stdio: "ignore" as const, }; diff --git a/src/session.ts b/src/session.ts index 96a9adb5..3a6e13ba 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2124,7 +2124,23 @@ ${skillMd} return; } - launchNotifyScript(notifyCommand, Date.now() - startedAt, this.projectRoot, undefined, configuredEnv); + // Find the last assistant message body for the BODY env variable. + let body: string | undefined; + const messages = this.listSessionMessages(sessionId); + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + if (msg && msg.role === "assistant" && msg.content) { + body = msg.content; + break; + } + } + + launchNotifyScript(notifyCommand, Date.now() - startedAt, this.projectRoot, undefined, configuredEnv, { + status: session.status, + failReason: session.failReason ?? undefined, + body, + title: session.summary ?? undefined, + }); } private addSessionProcess(sessionId: string, processId: string | number, command: string): void { diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index b7eadaeb..d079949c 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -783,6 +783,68 @@ test("reporting a new prompt does not warn when the background request fails", a assert.deepEqual(warnings, []); }); +test( + "SessionManager notifies successful completion with session context", + { skip: process.platform === "win32" }, + async () => { + const workspace = createTempDir("deepcode-notify-success-workspace-"); + const home = createTempDir("deepcode-notify-success-home-"); + setHomeDir(home); + + const notifyOutput = path.join(workspace, "notify.jsonl"); + const notifyScript = createNotifyRecorderScript(workspace); + const manager = createNotifyingSessionManager( + workspace, + [createChatResponse("final answer", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 })], + notifyScript, + notifyOutput + ); + + await manager.createSession({ text: "notify success" }); + + const records = await waitForNotifyRecords(notifyOutput, 1); + assert.equal(records[0]?.STATUS, "completed"); + assert.equal(records[0]?.FAIL_REASON, null); + assert.equal(records[0]?.BODY, "final answer"); + assert.equal(records[0]?.TITLE, "notify success"); + assert.match(String(records[0]?.DURATION), /^\d+$/); + } +); + +test( + "SessionManager notifies failed completion with failure context", + { skip: process.platform === "win32" }, + async () => { + const workspace = createTempDir("deepcode-notify-failure-workspace-"); + const home = createTempDir("deepcode-notify-failure-home-"); + setHomeDir(home); + + const notifyOutput = path.join(workspace, "notify.jsonl"); + const notifyScript = createNotifyRecorderScript(workspace); + const manager = createNotifyingSessionManager( + workspace, + [ + createChatResponse("first answer", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }), + new Error("second request failed"), + ], + notifyScript, + notifyOutput + ); + + const sessionId = await manager.createSession({ text: "notify failure" }); + await waitForNotifyRecords(notifyOutput, 1); + await manager.replySession(sessionId, { text: "second prompt" }); + + const records = await waitForNotifyRecords(notifyOutput, 2); + const failedRecord = records[1]; + assert.equal(failedRecord?.STATUS, "failed"); + assert.equal(failedRecord?.FAIL_REASON, "second request failed"); + assert.equal(failedRecord?.BODY, "first answer"); + assert.notEqual(failedRecord?.BODY, "stale-body"); + assert.equal(failedRecord?.TITLE, "notify failure"); + } +); + test("replySession continues without appending /continue as a user message", async () => { const workspace = createTempDir("deepcode-continue-workspace-"); const home = createTempDir("deepcode-continue-home-"); @@ -1657,6 +1719,49 @@ function createSessionManager(projectRoot: string, machineId: string): SessionMa }); } +function createNotifyingSessionManager( + projectRoot: string, + responses: unknown[], + notifyPath: string, + notifyOutput: string +): SessionManager { + const client = { + chat: { + completions: { + create: async () => { + const response = responses.shift(); + assert.ok(response, "expected a queued chat response"); + if (response instanceof Error) { + throw response; + } + return response; + }, + }, + }, + }; + + return new SessionManager({ + projectRoot, + createOpenAIClient: () => ({ + client: client as any, + model: "test-model", + baseURL: "https://api.deepseek.com", + thinkingEnabled: false, + notify: notifyPath, + env: { + NOTIFY_OUTPUT: notifyOutput, + STATUS: "stale-status", + FAIL_REASON: "stale-failure", + BODY: "stale-body", + TITLE: "stale-title", + }, + }), + getResolvedSettings: () => ({ model: "test-model" }), + renderMarkdown: (text) => text, + onAssistantMessage: () => {}, + }); +} + function createMockedClientSessionManager(projectRoot: string, responses: unknown[]): SessionManager { const client = { chat: { @@ -1740,6 +1845,45 @@ function createTempDir(prefix: string): string { return dir; } +function createNotifyRecorderScript(dir: string): string { + const scriptPath = path.join(dir, "notify-recorder.cjs"); + fs.writeFileSync( + scriptPath, + `#!/usr/bin/env node +const fs = require("fs"); +const keys = ["DURATION", "STATUS", "FAIL_REASON", "BODY", "TITLE"]; +const record = {}; +for (const key of keys) { + record[key] = Object.prototype.hasOwnProperty.call(process.env, key) ? process.env[key] : null; +} +fs.appendFileSync(process.env.NOTIFY_OUTPUT, JSON.stringify(record) + "\\n", "utf8"); +`, + "utf8" + ); + fs.chmodSync(scriptPath, 0o755); + return scriptPath; +} + +async function waitForNotifyRecords( + outputPath: string, + expectedCount: number +): Promise>> { + for (let attempt = 0; attempt < 100; attempt += 1) { + if (fs.existsSync(outputPath)) { + const records = fs + .readFileSync(outputPath, "utf8") + .split(/\r?\n/) + .filter(Boolean) + .map((line) => JSON.parse(line) as Record); + if (records.length >= expectedCount) { + return records; + } + } + await new Promise((resolve) => setTimeout(resolve, 20)); + } + assert.fail(`expected ${expectedCount} notify records in ${outputPath}`); +} + function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts index 6990288e..202f849a 100644 --- a/src/tests/settings-and-notify.test.ts +++ b/src/tests/settings-and-notify.test.ts @@ -1,6 +1,12 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { buildNotifyEnv, formatDurationSeconds, launchNotifyScript, type NotifySpawn } from "../common/notify"; +import { + buildNotifyEnv, + formatDurationSeconds, + launchNotifyScript, + type NotifyContext, + type NotifySpawn, +} from "../common/notify"; import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } from "../settings"; const TEST_PROCESS_ENV = {}; @@ -358,14 +364,52 @@ test("formatDurationSeconds preserves sub-second precision and trims trailing ze assert.equal(formatDurationSeconds(4000), "4"); }); -test("buildNotifyEnv injects DURATION", () => { +test("buildNotifyEnv injects DURATION without context", () => { const env = buildNotifyEnv(2750, { HOME: "/tmp/home" }); assert.equal(env.HOME, "/tmp/home"); assert.equal(env.DURATION, "2"); + assert.equal(env.STATUS, undefined); + assert.equal(env.FAIL_REASON, undefined); + assert.equal(env.BODY, undefined); + assert.equal(env.TITLE, undefined); +}); + +test("buildNotifyEnv injects STATUS, FAIL_REASON, BODY, and TITLE from context", () => { + const context: NotifyContext = { + status: "failed", + failReason: "API key not found", + body: "Hello, this is the last assistant message.", + title: "Fix login bug", + }; + const env = buildNotifyEnv(5000, { HOME: "/tmp/home" }, context); + assert.equal(env.HOME, "/tmp/home"); + assert.equal(env.DURATION, "5"); + assert.equal(env.STATUS, "failed"); + assert.equal(env.FAIL_REASON, "API key not found"); + assert.equal(env.BODY, "Hello, this is the last assistant message."); + assert.equal(env.TITLE, "Fix login bug"); +}); + +test("buildNotifyEnv omits optional context fields when not provided", () => { + const env = buildNotifyEnv( + 1000, + { + HOME: "/tmp/home", + STATUS: "stale-status", + FAIL_REASON: "stale-failure", + BODY: "stale-body", + TITLE: "stale-title", + }, + { status: "completed" } + ); + assert.equal(env.STATUS, "completed"); + assert.equal(env.FAIL_REASON, undefined); + assert.equal(env.BODY, undefined); + assert.equal(env.TITLE, undefined); }); test( - "launchNotifyScript passes DURATION and falls back to /bin/sh for non-executable scripts", + "launchNotifyScript passes DURATION, context vars, and falls back to /bin/sh for non-executable scripts", { skip: process.platform === "win32" }, () => { const calls: Array<{ @@ -390,7 +434,13 @@ test( }; }; - launchNotifyScript("/tmp/notify.sh", 2750, "/tmp/project", spawnProcess, { WEBHOOK: "configured" }); + const context: NotifyContext = { + status: "completed", + body: "Task finished successfully.", + title: "Fix login bug", + }; + + launchNotifyScript("/tmp/notify.sh", 2750, "/tmp/project", spawnProcess, { WEBHOOK: "configured" }, context); assert.equal(calls.length, 2); assert.equal(calls[0]?.command, "/tmp/notify.sh"); @@ -398,9 +448,16 @@ test( assert.equal(calls[0]?.options.cwd, "/tmp/project"); assert.equal(calls[0]?.options.env?.DURATION, "2"); assert.equal(calls[0]?.options.env?.WEBHOOK, "configured"); + assert.equal(calls[0]?.options.env?.STATUS, "completed"); + assert.equal(calls[0]?.options.env?.FAIL_REASON, undefined); + assert.equal(calls[0]?.options.env?.BODY, "Task finished successfully."); + assert.equal(calls[0]?.options.env?.TITLE, "Fix login bug"); assert.equal(calls[1]?.command, "/bin/sh"); assert.deepEqual(calls[1]?.args, ["/tmp/notify.sh"]); assert.equal(calls[1]?.options.cwd, "/tmp/project"); assert.equal(calls[1]?.options.env?.DURATION, "2"); + assert.equal(calls[1]?.options.env?.STATUS, "completed"); + assert.equal(calls[1]?.options.env?.BODY, "Task finished successfully."); + assert.equal(calls[1]?.options.env?.TITLE, "Fix login bug"); } ); From a3ff70e82d548a8c1273ea377844f078cbd0ae00 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 10:43:55 +0800 Subject: [PATCH 013/212] docs(notify): add Windows Terminal, Linux, and msg popup notification examples; add edge-case tests - Expand OSC 9 example to cover both iTerm2 and Windows Terminal - Add .bat example for Windows Terminal users - Add Linux notify-send example - Add Windows msg popup notification example - Add tests for empty-string rejection and special character preservation --- docs/configuration.md | 32 +++++++++++++++++++++++---- docs/configuration_en.md | 32 +++++++++++++++++++++++---- src/tests/settings-and-notify.test.ts | 27 ++++++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 45aaab06..7c2880cc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -83,14 +83,14 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 } ``` -**iTerm2 终端通知示例**: +**终端内通知示例(支持 iTerm2 / Windows Terminal)**: -如果你的终端是 iTerm2,可以直接通过 OSC 9 转义序列弹出通知,无需额外脚本。创建以下脚本(如 `~/.deepcode/notify.sh`): +如果你的终端是 iTerm2 或 Windows Terminal,可以直接通过 OSC 9 转义序列弹出终端原生通知,无需额外依赖。创建以下脚本(如 `~/.deepcode/notify.sh`): ```bash #!/bin/bash -# iTerm2 OSC 9 通知 -echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07" +# iTerm2 / Windows Terminal OSC 9 通知 +printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}" ``` ```json @@ -99,6 +99,14 @@ echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07" } ``` +Windows 用户如使用 Git Bash,上述脚本同样可用;也可创建 `.bat` 脚本: + +```batch +@echo off +REM Windows Terminal OSC 9 通知 +echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07 +``` + **macOS 系统通知示例**: ```bash @@ -107,6 +115,22 @@ echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07" osascript -e "display notification \"任务已${STATUS:-完成},耗时 ${DURATION}s\" with title \"DeepCode\"" ``` +**Linux 系统通知示例**(需安装 `libnotify-bin`): + +```bash +#!/bin/bash +# Linux notify-send 通知 +notify-send "DeepCode" "任务已${STATUS:-完成},耗时 ${DURATION}s" +``` + +**Windows msg 弹窗通知示例**: + +```batch +@echo off +REM Windows msg 弹窗通知 +msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)" +``` + #### `webSearchTool` — 自定义联网搜索 Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索逻辑,可将 `webSearchTool` 设为一个可执行脚本的完整路径: diff --git a/docs/configuration_en.md b/docs/configuration_en.md index 606fcab9..5d931f4e 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -83,14 +83,14 @@ The following context is injected as environment variables when the notify scrip } ``` -**iTerm2 Notification Example**: +**Terminal Notification Example (iTerm2 / Windows Terminal)**: -On iTerm2 you can use the OSC 9 escape sequence for native notifications. Create a script (e.g., `~/.deepcode/notify.sh`): +On iTerm2 or Windows Terminal you can use the OSC 9 escape sequence for native terminal notifications with zero dependencies. Create a script (e.g., `~/.deepcode/notify.sh`): ```bash #!/bin/bash -# iTerm2 OSC 9 notification -echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07" +# iTerm2 / Windows Terminal OSC 9 notification +printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}" ``` ```json @@ -99,6 +99,14 @@ echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07" } ``` +Windows users on Git Bash can use the same script; alternatively create a `.bat` script: + +```batch +@echo off +REM Windows Terminal OSC 9 notification +echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07 +``` + **macOS System Notification Example**: ```bash @@ -107,6 +115,22 @@ echo -e "\x1b]9;DeepCode: task ${STATUS:-completed} (${DURATION}s)\x07" osascript -e "display notification \"Task ${STATUS:-completed}, took ${DURATION}s\" with title \"DeepCode\"" ``` +**Linux System Notification Example** (requires `libnotify-bin`): + +```bash +#!/bin/bash +# Linux notify-send notification +notify-send "DeepCode" "Task ${STATUS:-completed}, took ${DURATION}s" +``` + +**Windows msg Popup Notification Example**: + +```batch +@echo off +REM Windows msg popup notification +msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)" +``` + #### `webSearchTool` — Custom Web Search Deep Code has a built-in, free-to-use Web Search tool. If you need custom search logic, set `webSearchTool` to the full path of an executable script: diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts index 202f849a..1707aff8 100644 --- a/src/tests/settings-and-notify.test.ts +++ b/src/tests/settings-and-notify.test.ts @@ -408,6 +408,33 @@ test("buildNotifyEnv omits optional context fields when not provided", () => { assert.equal(env.TITLE, undefined); }); +test("buildNotifyEnv ignores empty strings in context", () => { + const env = buildNotifyEnv( + 1000, + { HOME: "/tmp/home" }, + { + status: "", + failReason: "", + body: "", + title: "", + } + ); + assert.equal(env.STATUS, undefined); + assert.equal(env.FAIL_REASON, undefined); + assert.equal(env.BODY, undefined); + assert.equal(env.TITLE, undefined); +}); + +test("buildNotifyEnv preserves special characters in body and title", () => { + const context: NotifyContext = { + body: 'Line 1\nLine 2\tindented "quoted"', + title: "Fix: login & signup (urgent)", + }; + const env = buildNotifyEnv(1000, {}, context); + assert.equal(env.BODY, 'Line 1\nLine 2\tindented "quoted"'); + assert.equal(env.TITLE, "Fix: login & signup (urgent)"); +}); + test( "launchNotifyScript passes DURATION, context vars, and falls back to /bin/sh for non-executable scripts", { skip: process.platform === "win32" }, From 479606f6a7087398302334996e95cb8eb2d841b3 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 13:33:28 +0800 Subject: [PATCH 014/212] docs(notify): replace terminal notification examples with Feishu webhook example - Remove iTerm2/Windows Terminal OSC 9, macOS osascript, Linux notify-send, and Windows msg examples (OSC 9 is not compatible with current spawn+stdio:ignore architecture) - Add Feishu (Lark) webhook notification example in both Chinese and English docs - Keep the env variable table (DURATION, STATUS, FAIL_REASON, BODY, TITLE) unchanged --- docs/configuration.md | 62 ++++++++++++++-------------------------- docs/configuration_en.md | 62 ++++++++++++++-------------------------- 2 files changed, 44 insertions(+), 80 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7c2880cc..b05a44f3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -83,53 +83,35 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 } ``` -**终端内通知示例(支持 iTerm2 / Windows Terminal)**: +**飞书 Webhook 通知示例**: -如果你的终端是 iTerm2 或 Windows Terminal,可以直接通过 OSC 9 转义序列弹出终端原生通知,无需额外依赖。创建以下脚本(如 `~/.deepcode/notify.sh`): +`node` 构建 JSON(自动转义特殊字符),`curl` 发送: ```bash #!/bin/bash -# iTerm2 / Windows Terminal OSC 9 通知 -printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}" -``` - -```json -{ - "notify": "/Users/you/.deepcode/notify.sh" -} -``` - -Windows 用户如使用 Git Bash,上述脚本同样可用;也可创建 `.bat` 脚本: - -```batch -@echo off -REM Windows Terminal OSC 9 通知 -echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07 -``` - -**macOS 系统通知示例**: - -```bash -#!/bin/bash -# macOS 系统通知 -osascript -e "display notification \"任务已${STATUS:-完成},耗时 ${DURATION}s\" with title \"DeepCode\"" -``` - -**Linux 系统通知示例**(需安装 `libnotify-bin`): +WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx" + +STATUS="${STATUS:-completed}" +TITLE="${TITLE:-Untitled}" +DURATION="${DURATION:-0}" +BODY="${BODY:-(no output)}" + +PAYLOAD=$(node -e " +process.stdout.write(JSON.stringify({ + msg_type: 'interactive', + card: { + header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } }, + elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }] + } +})) +") -```bash -#!/bin/bash -# Linux notify-send 通知 -notify-send "DeepCode" "任务已${STATUS:-完成},耗时 ${DURATION}s" +curl -s -X POST "$WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" ``` -**Windows msg 弹窗通知示例**: - -```batch -@echo off -REM Windows msg 弹窗通知 -msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)" -``` +将 `WEBHOOK_URL` 替换为你的飞书机器人 Webhook 地址。更多变量参考上表。同样适用于 Slack、企业微信等 webhook 类通知,只需修改 JSON payload 格式。 #### `webSearchTool` — 自定义联网搜索 diff --git a/docs/configuration_en.md b/docs/configuration_en.md index 5d931f4e..4f2f94de 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -83,53 +83,35 @@ The following context is injected as environment variables when the notify scrip } ``` -**Terminal Notification Example (iTerm2 / Windows Terminal)**: +**Feishu (Lark) Webhook Notification Example**: -On iTerm2 or Windows Terminal you can use the OSC 9 escape sequence for native terminal notifications with zero dependencies. Create a script (e.g., `~/.deepcode/notify.sh`): +`node` builds the JSON (auto-escapes special characters), `curl` sends it: ```bash #!/bin/bash -# iTerm2 / Windows Terminal OSC 9 notification -printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}" -``` - -```json -{ - "notify": "/Users/you/.deepcode/notify.sh" -} -``` - -Windows users on Git Bash can use the same script; alternatively create a `.bat` script: - -```batch -@echo off -REM Windows Terminal OSC 9 notification -echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07 -``` - -**macOS System Notification Example**: - -```bash -#!/bin/bash -# macOS system notification -osascript -e "display notification \"Task ${STATUS:-completed}, took ${DURATION}s\" with title \"DeepCode\"" -``` - -**Linux System Notification Example** (requires `libnotify-bin`): +WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx" + +STATUS="${STATUS:-completed}" +TITLE="${TITLE:-Untitled}" +DURATION="${DURATION:-0}" +BODY="${BODY:-(no output)}" + +PAYLOAD=$(node -e " +process.stdout.write(JSON.stringify({ + msg_type: 'interactive', + card: { + header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } }, + elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }] + } +})) +") -```bash -#!/bin/bash -# Linux notify-send notification -notify-send "DeepCode" "Task ${STATUS:-completed}, took ${DURATION}s" +curl -s -X POST "$WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" ``` -**Windows msg Popup Notification Example**: - -```batch -@echo off -REM Windows msg popup notification -msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)" -``` +Replace `WEBHOOK_URL` with your Feishu bot webhook URL. See the table above for all available variables. This pattern also works for other webhook-based notifications (Slack, WeCom, etc.) — just adjust the JSON payload format. #### `webSearchTool` — Custom Web Search From 7e5eeda26829b14eb3ed503b550db06c1145acf6 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 19 May 2026 15:05:00 +0800 Subject: [PATCH 015/212] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=20raw=20?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=B6=88=E6=81=AF=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 Raw 模式下,使用 process.stdout.write 直接输出所有可见消息 - 清屏并重置光标位置,避免 Ink 组件干扰 - 显示提示信息,指导用户按 ESC 退出 raw 模式 - 优化终端尺寸变化时的重绘逻辑 - 更新依赖,确保 raw 模式变动触发重新渲染 --- src/ui/App.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 9189df6c..e39fd03e 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -434,8 +434,31 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. } lastRenderedColumnsRef.current = stableColumns; + if (mode === RawMode.Raw) { + // In raw mode, re-render all messages directly to stdout at the new width. + // Use process.stdout.write instead of writeRef to avoid Ink interference. + process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); + const activeSessionId = sessionManager.getActiveSessionId(); + const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : []; + for (const msg of allMessages) { + process.stdout.write("\n"); + process.stdout.write(renderMessageToStdout(msg, mode) + "\n\n"); + } + if (allMessages.length > 0) { + process.stdout.write("\n\n"); + process.stdout.write(chalk.dim("Press ESC to exit raw mode")); + } else { + process.stdout.write("\n"); + process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)")); + process.stdout.write("\n\n"); + process.stdout.write(chalk.dim("Press ESC to exit raw mode")); + } + return; + } + // Force full redraw on terminal resize to avoid stale wrapped rows. writeRef.current("\u001B[2J\u001B[H"); + setMessages([]); setShowWelcome(false); setWelcomeNonce((n) => n + 1); @@ -447,7 +470,8 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setMessages(nextMessages); setShowWelcome(true); }, 0); - }, [busy, sessionManager, stableColumns, stdout]); + }, [busy, mode, sessionManager, stableColumns, stdout]); + const screenWidth = useMemo(() => stableColumns ?? stdout?.columns ?? 80, [stableColumns, stdout]); const promptHistory = useMemo(() => { return messages From faf10c3e087d214bf863e9df14040176e30de821 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 19 May 2026 15:12:08 +0800 Subject: [PATCH 016/212] =?UTF-8?q?refactor(ui):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=AE=BD=E5=BA=A6=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=92=8C=E5=BC=95=E7=94=A8=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合并并调整了关于窗口宽度columns的使用,去除了stableColumns状态 - 引用lastRenderedColumnsRef改为直接使用columns,避免延迟更新 - 将多个相关的useRef(writeRef、rawModeRef、messagesRef、processStdoutRef)移至同一位置声明 - 调整useEffect依赖项,改为监听columns代替stableColumns - 优化RawMode下消息重绘逻辑,确保宽度变化时重新渲染 - 统一了screenWidth的计算逻辑,简化代码结构 --- src/ui/App.tsx | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index e39fd03e..582abaff 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -55,7 +55,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. const { exit } = useApp(); const { stdout, write } = useStdout(); const { columns } = useWindowSize(); + const { mode, setMode } = useRawModeContext(); const initialPromptSubmittedRef = useRef(false); + const processStdoutRef = useRef>(new Map()); + const rawModeRef = useRef(mode); + const writeRef = useRef(write); + const lastRenderedColumnsRef = useRef(null); + const messagesRef = useRef([]); const [view, setView] = useState("chat"); const [busy, setBusy] = useState(false); const [skills, setSkills] = useState([]); @@ -74,13 +80,8 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. const [nowTick, setNowTick] = useState(0); const [mcpStatuses, setMcpStatuses] = useState>([]); const [showProcessStdout, setShowProcessStdout] = useState(false); - const processStdoutRef = useRef>(new Map()); - const { mode, setMode } = useRawModeContext(); - const rawModeRef = useRef(mode); rawModeRef.current = mode; - - const messagesRef = useRef([]); messagesRef.current = messages; const sessionManager = useMemo(() => { @@ -172,7 +173,6 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. }; }, [sessionManager]); - const writeRef = useRef(write); writeRef.current = write; const handlePrompt = useCallback( async (submission: PromptSubmission) => { @@ -412,27 +412,21 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. [handleSelectSession, sessionManager, setMode] ); - const [stableColumns, setStableColumns] = useState(columns); - useEffect(() => { - const timer = setTimeout(() => setStableColumns(columns), 100); - return () => clearTimeout(timer); - }, [columns]); - const lastRenderedColumnsRef = useRef(null); useEffect(() => { if (!stdout?.isTTY) { return; } - if (stableColumns <= 0) { + if (columns <= 0) { return; } if (lastRenderedColumnsRef.current === null) { - lastRenderedColumnsRef.current = stableColumns; + lastRenderedColumnsRef.current = columns; return; } - if (lastRenderedColumnsRef.current === stableColumns) { + if (lastRenderedColumnsRef.current === columns) { return; } - lastRenderedColumnsRef.current = stableColumns; + lastRenderedColumnsRef.current = columns; if (mode === RawMode.Raw) { // In raw mode, re-render all messages directly to stdout at the new width. @@ -470,9 +464,9 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setMessages(nextMessages); setShowWelcome(true); }, 0); - }, [busy, mode, sessionManager, stableColumns, stdout]); + }, [busy, mode, sessionManager, columns, stdout]); - const screenWidth = useMemo(() => stableColumns ?? stdout?.columns ?? 80, [stableColumns, stdout]); + const screenWidth = useMemo(() => columns ?? stdout?.columns ?? 80, [columns, stdout]); const promptHistory = useMemo(() => { return messages .filter((message) => message.role === "user" && typeof message.content === "string") From 32da2ca695e0ff3e135dcbd591ca156554e97108 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 19 May 2026 15:28:38 +0800 Subject: [PATCH 017/212] =?UTF-8?q?feat(rawmode):=20=E6=B7=BB=E5=8A=A0=20R?= =?UTF-8?q?awMode=20=E6=8F=8F=E8=BF=B0=E4=BF=A1=E6=81=AF=E4=BB=A5=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/contexts/RawModeContext.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/contexts/RawModeContext.tsx b/src/ui/contexts/RawModeContext.tsx index 6fbc7063..3198a3ae 100644 --- a/src/ui/contexts/RawModeContext.tsx +++ b/src/ui/contexts/RawModeContext.tsx @@ -10,14 +10,17 @@ export const RAW_COMMAND_MODELS: DropdownMenuItem[] = [ { label: "Lite mode", key: RawMode.Lite, + description: "Collapse chain-of-thought reasoning.", }, { label: "Normal mode", key: RawMode.None, + description: "Show full chain-of-thought reasoning.", }, { label: "Raw scrollback mode", key: RawMode.Raw, + description: "Show scrollback mode for copy-friendly terminal selection.", }, ] as const; From f9d2737f2737d195ab94e587d378c322f13b411a Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 19 May 2026 16:04:03 +0800 Subject: [PATCH 018/212] docs(notify): extract notification examples to standalone notify.md - Add docs/notify.md and docs/notify_en.md with Slack, Feishu, terminal, macOS, Linux, Windows msg, and custom notification examples - Simplify notify section in configuration.md / configuration_en.md to field description + env table + reference to notify docs - Replace external binfer.net link with docs/notify.md in README FAQ across README.md, README-zh_CN.md, README-en.md --- README-en.md | 2 +- README-zh_CN.md | 2 +- README.md | 2 +- docs/configuration.md | 32 +----- docs/configuration_en.md | 32 +----- docs/notify.md | 211 +++++++++++++++++++++++++++++++++++++++ docs/notify_en.md | 211 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 429 insertions(+), 63 deletions(-) create mode 100644 docs/notify.md create mode 100644 docs/notify_en.md diff --git a/README-en.md b/README-en.md index 4c78cbd2..55d0cf69 100644 --- a/README-en.md +++ b/README-en.md @@ -99,7 +99,7 @@ Deep Code supports multimodal input — you can paste images from the clipboard ### How to automatically send a Slack message after a task completes? -Write a shell notification script that calls a Slack webhook, then set the `notify` field in `~/.deepcode/settings.json` to the full path of the script. For detailed steps, refer to: https://binfer.net/share/jby5xnc-so6g +Write a shell notification script that calls a Slack webhook, then set the `notify` field in `~/.deepcode/settings.json` to the full path of the script. For detailed steps, see [docs/notify_en.md](docs/notify_en.md). ### How do I enable web search? diff --git a/README-zh_CN.md b/README-zh_CN.md index 98346b65..8a427def 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -99,7 +99,7 @@ Deep Code 支持多模态,可使用ctrl+v从剪贴板粘贴图片。但目前 ### 怎样在任务完成后自动给 Slack 发消息? -编写一个调用 Slack webhook 的 Shell 通知脚本,然后在 `~/.deepcode/settings.json` 中将 `notify` 字段设为该脚本的完整路径即可。详细步骤可参考:https://binfer.net/share/jby5xnc-so6g +编写一个调用 Slack webhook 的 Shell 通知脚本,然后在 `~/.deepcode/settings.json` 中将 `notify` 字段设为该脚本的完整路径即可。详细步骤请参考 [docs/notify.md](docs/notify.md)。 ### 怎样启用联网搜索功能? diff --git a/README.md b/README.md index 98346b65..8a427def 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Deep Code 支持多模态,可使用ctrl+v从剪贴板粘贴图片。但目前 ### 怎样在任务完成后自动给 Slack 发消息? -编写一个调用 Slack webhook 的 Shell 通知脚本,然后在 `~/.deepcode/settings.json` 中将 `notify` 字段设为该脚本的完整路径即可。详细步骤可参考:https://binfer.net/share/jby5xnc-so6g +编写一个调用 Slack webhook 的 Shell 通知脚本,然后在 `~/.deepcode/settings.json` 中将 `notify` 字段设为该脚本的完整路径即可。详细步骤请参考 [docs/notify.md](docs/notify.md)。 ### 怎样启用联网搜索功能? diff --git a/docs/configuration.md b/docs/configuration.md index b05a44f3..1cce9a14 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -79,39 +79,11 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 ```json { - "notify": "/path/to/slack-notify.sh" + "notify": "/path/to/notify-script.sh" } ``` -**飞书 Webhook 通知示例**: - -`node` 构建 JSON(自动转义特殊字符),`curl` 发送: - -```bash -#!/bin/bash -WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx" - -STATUS="${STATUS:-completed}" -TITLE="${TITLE:-Untitled}" -DURATION="${DURATION:-0}" -BODY="${BODY:-(no output)}" - -PAYLOAD=$(node -e " -process.stdout.write(JSON.stringify({ - msg_type: 'interactive', - card: { - header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } }, - elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }] - } -})) -") - -curl -s -X POST "$WEBHOOK_URL" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD" -``` - -将 `WEBHOOK_URL` 替换为你的飞书机器人 Webhook 地址。更多变量参考上表。同样适用于 Slack、企业微信等 webhook 类通知,只需修改 JSON payload 格式。 +> 详细的 Slack、飞书、终端通知、系统通知等配置示例,请参阅 [notify.md](notify.md)。 #### `webSearchTool` — 自定义联网搜索 diff --git a/docs/configuration_en.md b/docs/configuration_en.md index 4f2f94de..fa396f90 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -79,39 +79,11 @@ The following context is injected as environment variables when the notify scrip ```json { - "notify": "/path/to/slack-notify.sh" + "notify": "/path/to/notify-script.sh" } ``` -**Feishu (Lark) Webhook Notification Example**: - -`node` builds the JSON (auto-escapes special characters), `curl` sends it: - -```bash -#!/bin/bash -WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx" - -STATUS="${STATUS:-completed}" -TITLE="${TITLE:-Untitled}" -DURATION="${DURATION:-0}" -BODY="${BODY:-(no output)}" - -PAYLOAD=$(node -e " -process.stdout.write(JSON.stringify({ - msg_type: 'interactive', - card: { - header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } }, - elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }] - } -})) -") - -curl -s -X POST "$WEBHOOK_URL" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD" -``` - -Replace `WEBHOOK_URL` with your Feishu bot webhook URL. See the table above for all available variables. This pattern also works for other webhook-based notifications (Slack, WeCom, etc.) — just adjust the JSON payload format. +> For detailed configuration examples (Slack, Feishu, terminal notifications, system notifications, etc.), see [notify_en.md](notify_en.md). #### `webSearchTool` — Custom Web Search diff --git a/docs/notify.md b/docs/notify.md new file mode 100644 index 00000000..d73eef45 --- /dev/null +++ b/docs/notify.md @@ -0,0 +1,211 @@ +# Deep Code 任务完成通知 + +当 AI 助手完成一轮任务后,Deep Code 可以自动执行一个通知脚本,将任务结果发送到你指定的渠道(如 Slack、系统通知等)。 + +## 工作原理 + +在 `settings.json` 中配置 `notify` 字段,指向一个可执行脚本的完整路径。每次 AI 助手完成任务应答后,Deep Code 会执行该脚本,并通过环境变量注入上下文信息。 + +## 注入的环境变量 + +| 环境变量 | 说明 | +|----------|------| +| `DURATION` | 会话耗时,单位秒(整数) | +| `STATUS` | 会话状态:`"completed"` 或 `"failed"` | +| `FAIL_REASON` | 失败原因(仅失败时设置) | +| `BODY` | 最后一条 AI 助手回复的文本内容 | +| `TITLE` | 会话标题(对应 resume 列表中的标题) | + +## 配置方法 + +编辑 `~/.deepcode/settings.json`,添加 `notify` 字段: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max", + "notify": "/path/to/your-notify-script.sh" +} +``` + +你也可以在 `env` 中配置通知脚本所需的自定义环境变量,例如 Slack Webhook URL: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-...", + "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/*****/****/**********" + }, + "notify": "/Users/you/.deepcode/notify-slack.sh" +} +``` + +这些 `env` 中的变量会被注入到脚本的执行环境中。 + +## Slack 通知 + +### 1. 获取 Slack Webhook URL + +1. 创建 [Slack App](https://api.slack.com/apps) +2. 在 App 页面点击 **Incoming Webhooks** → **Add New Webhook to Workspace**,生成 Webhook URL + +### 2. 创建通知脚本 + +创建 `~/.deepcode/notify-slack.sh`: + +```bash +#!/usr/bin/env bash +SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}" +CURRENT_DIR=$(pwd) +BRANCH=$(git branch --show-current 2>/dev/null) +curl -X POST "$SLACK_WEBHOOK_URL" \ + -H "Content-type: application/json" \ + --data "{ + \"text\": \"✅ Deep Code 任务已完成\n · cwd: $CURRENT_DIR\n · Branch: $BRANCH\n · Duration: $DURATION 秒\" + }" +``` + +给脚本添加可执行权限: + +```bash +chmod +x ~/.deepcode/notify-slack.sh +``` + +### 3. 配置 settings.json + +```json +{ + "env": { + "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/*****/****/**********" + }, + "notify": "/Users/you/.deepcode/notify-slack.sh" +} +``` + +> Python 版本的脚本同样支持,你可以在 `env` 中传入并引用任意自定义环境变量。 + +## 飞书 / 企业微信等 Webhook 通知 + +以下示例使用 `node` 构建 JSON(自动转义特殊字符),`curl` 发送。通过 `env` 传入 `WEBHOOK_URL`: + +```bash +#!/bin/bash +WEBHOOK_URL="${WEBHOOK_URL:-}" + +STATUS="${STATUS:-completed}" +TITLE="${TITLE:-Untitled}" +DURATION="${DURATION:-0}" +BODY="${BODY:-(no output)}" + +PAYLOAD=$(node -e " +process.stdout.write(JSON.stringify({ + msg_type: 'interactive', + card: { + header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } }, + elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }] + } +})) +") + +curl -s -X POST "$WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" +``` + +```json +{ + "env": { + "WEBHOOK_URL": "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx" + }, + "notify": "/Users/you/.deepcode/notify-feishu.sh" +} +``` + +将 `WEBHOOK_URL` 替换为你的飞书机器人 Webhook 地址。此模式同样适用于 Slack、企业微信等 webhook 类通知,只需修改 JSON payload 格式。 + +## 终端通知(iTerm2 / Windows Terminal) + +如果你的终端是 iTerm2 或 Windows Terminal,可以直接通过 OSC 9 转义序列弹出终端原生通知,无需额外依赖。 + +创建 `~/.deepcode/notify.sh`: + +```bash +#!/bin/bash +# iTerm2 / Windows Terminal OSC 9 通知 +printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +Windows 用户如使用 Git Bash,上述脚本同样可用;也可创建 `.bat` 脚本: + +```batch +@echo off +REM Windows Terminal OSC 9 通知 +echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07 +``` + +## macOS 系统通知 + +```bash +#!/bin/bash +# macOS 系统通知 +osascript -e "display notification \"任务已${STATUS:-完成},耗时 ${DURATION}s\" with title \"DeepCode\"" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +## Linux 系统通知 + +需要安装 `libnotify-bin`: + +```bash +sudo apt install libnotify-bin # Debian/Ubuntu +``` + +创建 `~/.deepcode/notify.sh`: + +```bash +#!/bin/bash +# Linux notify-send 通知 +notify-send "DeepCode" "任务已${STATUS:-完成},耗时 ${DURATION}s" +``` + +```json +{ + "notify": "/home/you/.deepcode/notify.sh" +} +``` + +## Windows msg 弹窗通知 + +```batch +@echo off +REM Windows msg 弹窗通知 +msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)" +``` + +```json +{ + "notify": "C:\\Users\\you\\.deepcode\\notify.bat" +} +``` + +## 自定义通知脚本 + +你可以根据通知脚本注入的环境变量自行编写任意逻辑的通知脚本(Python、Node.js、Ruby 等均可),只要脚本可执行即可。脚本中可通过 `env` 字段传入额外需要的配置变量。 diff --git a/docs/notify_en.md b/docs/notify_en.md new file mode 100644 index 00000000..b949161c --- /dev/null +++ b/docs/notify_en.md @@ -0,0 +1,211 @@ +# Deep Code Task Completion Notification + +When the AI assistant finishes a round of tasks, Deep Code can automatically execute a notification script to send task results to your chosen channel (Slack, system notifications, etc.). + +## How It Works + +Configure the `notify` field in `settings.json` with the full path to an executable script. Every time the AI assistant completes a task response, Deep Code executes that script and injects context as environment variables. + +## Injected Environment Variables + +| Variable | Description | +|----------|-------------| +| `DURATION` | Session duration in seconds (integer) | +| `STATUS` | Session status: `"completed"` or `"failed"` | +| `FAIL_REASON` | Failure reason (only set on failure) | +| `BODY` | The text content of the last AI assistant reply | +| `TITLE` | Session title (matches the resume list title) | + +## Configuration + +Edit `~/.deepcode/settings.json` and add the `notify` field: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max", + "notify": "/path/to/your-notify-script.sh" +} +``` + +You can also configure custom environment variables for the notify script in `env`, such as a Slack Webhook URL: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-...", + "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/*****/****/**********" + }, + "notify": "/Users/you/.deepcode/notify-slack.sh" +} +``` + +These `env` variables are injected into the script's execution environment. + +## Slack Notification + +### 1. Get a Slack Webhook URL + +1. Create a [Slack App](https://api.slack.com/apps) +2. In the App page, go to **Incoming Webhooks** → **Add New Webhook to Workspace** to generate a Webhook URL + +### 2. Create the Notification Script + +Create `~/.deepcode/notify-slack.sh`: + +```bash +#!/usr/bin/env bash +SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}" +CURRENT_DIR=$(pwd) +BRANCH=$(git branch --show-current 2>/dev/null) +curl -X POST "$SLACK_WEBHOOK_URL" \ + -H "Content-type: application/json" \ + --data "{ + \"text\": \"✅ Deep Code task completed\n · cwd: $CURRENT_DIR\n · Branch: $BRANCH\n · Duration: $DURATION s\" + }" +``` + +Make the script executable: + +```bash +chmod +x ~/.deepcode/notify-slack.sh +``` + +### 3. Configure settings.json + +```json +{ + "env": { + "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/*****/****/**********" + }, + "notify": "/Users/you/.deepcode/notify-slack.sh" +} +``` + +> A Python version is also supported; you can pass and reference any custom environment variables via `env`. + +## Feishu / WeCom Webhook Notification + +Use `node` to build JSON (auto-escapes special characters) and `curl` to send. Pass `WEBHOOK_URL` via `env`: + +```bash +#!/bin/bash +WEBHOOK_URL="${WEBHOOK_URL:-}" + +STATUS="${STATUS:-completed}" +TITLE="${TITLE:-Untitled}" +DURATION="${DURATION:-0}" +BODY="${BODY:-(no output)}" + +PAYLOAD=$(node -e " +process.stdout.write(JSON.stringify({ + msg_type: 'interactive', + card: { + header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } }, + elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }] + } +})) +") + +curl -s -X POST "$WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" +``` + +```json +{ + "env": { + "WEBHOOK_URL": "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx" + }, + "notify": "/Users/you/.deepcode/notify-feishu.sh" +} +``` + +Replace `WEBHOOK_URL` with your Feishu bot webhook URL. This pattern also works for other webhook-based notifications (Slack, WeCom, etc.) — just adjust the JSON payload format. + +## Terminal Notification (iTerm2 / Windows Terminal) + +On iTerm2 or Windows Terminal, you can use the OSC 9 escape sequence for native terminal notifications with zero dependencies. + +Create `~/.deepcode/notify.sh`: + +```bash +#!/bin/bash +# iTerm2 / Windows Terminal OSC 9 notification +printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +Windows users on Git Bash can use the same script; alternatively, create a `.bat` script: + +```batch +@echo off +REM Windows Terminal OSC 9 notification +echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07 +``` + +## macOS System Notification + +```bash +#!/bin/bash +# macOS system notification +osascript -e "display notification \"Task ${STATUS:-completed}, took ${DURATION}s\" with title \"DeepCode\"" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +## Linux System Notification + +Requires `libnotify-bin`: + +```bash +sudo apt install libnotify-bin # Debian/Ubuntu +``` + +Create `~/.deepcode/notify.sh`: + +```bash +#!/bin/bash +# Linux notify-send notification +notify-send "DeepCode" "Task ${STATUS:-completed}, took ${DURATION}s" +``` + +```json +{ + "notify": "/home/you/.deepcode/notify.sh" +} +``` + +## Windows msg Popup Notification + +```batch +@echo off +REM Windows msg popup notification +msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)" +``` + +```json +{ + "notify": "C:\\Users\\you\\.deepcode\\notify.bat" +} +``` + +## Custom Notification Scripts + +You can write your own notification scripts in any language (Python, Node.js, Ruby, etc.) using the injected environment variables and any additional variables passed via `env`. From 1bd7e6a38c3f0f58a9b6971ba31d8c888d9f975d Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 16:24:40 +0800 Subject: [PATCH 019/212] perf: reuse OpenAI client instance and add connection warmup Cache the OpenAI client at module level keyed by (apiKey, baseURL) to avoid creating a fresh HTTP connection pool on every LLM turn. The client is a stateless fetch wrapper so sharing across calls is safe. Model, thinking-mode and other settings are still read fresh from config files each time. Also add a mount-time warmup effect that eagerly creates the client so the TCP+TLS connection is established while the user composes their first prompt. --- .gitignore | 1 + src/ui/App.tsx | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 11b67ce4..dd972a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ .vscode/ *.tgz *.log +scripts/ diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 582abaff..e82e5f31 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -162,6 +162,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. void refreshSkills(); }, [refreshSessionsList, refreshSkills]); + // Eagerly create the OpenAI client on mount so the TCP+TLS connection + // warmup (fire-and-forget inside createOpenAIClient) starts before the + // user sends their first prompt. + useEffect(() => { + createOpenAIClient(projectRoot); + }, [projectRoot]); + useLayoutEffect(() => { const settings = resolveCurrentSettings(projectRoot); void sessionManager.initMcpServers(settings.mcpServers); @@ -721,6 +728,13 @@ export function resolveCurrentSettings(projectRoot: string = process.cwd()): Res ); } +// Module-level cache for the OpenAI client instance. The client itself is +// a stateless fetch wrapper, so it is safe to share across calls as long as +// the apiKey + baseURL stay the same. Model, thinking-mode and other +// settings are always read fresh from the project / user config files. +let _cachedOpenAI: OpenAI | null = null; +let _cachedOpenAIKey = ""; + export function createOpenAIClient(projectRoot: string = process.cwd()): { client: OpenAI | null; model: string; @@ -749,12 +763,35 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { }; } - const client = new OpenAI({ + const cacheKey = `${settings.apiKey}::${settings.baseURL}`; + if (_cachedOpenAI && _cachedOpenAIKey === cacheKey) { + return { + client: _cachedOpenAI, + model: settings.model, + baseURL: settings.baseURL, + thinkingEnabled: settings.thinkingEnabled, + reasoningEffort: settings.reasoningEffort, + debugLogEnabled: settings.debugLogEnabled, + notify: settings.notify, + webSearchTool: settings.webSearchTool, + env: settings.env, + machineId: getMachineId(), + }; + } + + _cachedOpenAI = new OpenAI({ apiKey: settings.apiKey, baseURL: settings.baseURL || undefined, }); + _cachedOpenAIKey = cacheKey; + + // Fire-and-forget warmup: pre-establish TCP+TLS connection to the API + // server while the user is composing their first prompt. Errors are + // silently ignored — the real request will retry on its own if needed. + void _cachedOpenAI.models.list().catch(() => {}); + return { - client, + client: _cachedOpenAI, model: settings.model, baseURL: settings.baseURL, thinkingEnabled: settings.thinkingEnabled, From 2e5b2ed2a4eed8c463546385ecbf374002a8d6c6 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 16:54:30 +0800 Subject: [PATCH 020/212] perf: replace undici fetch with custom https.Agent for long keepAlive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default undici-based global fetch only keeps connections alive for 4 seconds, which is too short for a CLI where the user may spend 10–30 seconds reading output before typing the next prompt. Add a custom fetch implementation backed by node:https.Agent with keepAlive: true and a 60-second idle timeout. The custom fetch is passed to the OpenAI SDK constructor so every LLM API request benefits from persistent connections across conversational turns. Also handles streaming request bodies (ReadableStream) for SDK features like file uploads. --- src/ui/App.tsx | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index e82e5f31..1cdd8557 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; import * as fs from "fs"; +import https from "node:https"; import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; @@ -728,6 +729,99 @@ export function resolveCurrentSettings(projectRoot: string = process.cwd()): Res ); } +// Custom fetch implementation that uses node:https.Agent with a configurable +// keepAlive timeout. The default undici-based global fetch only keeps +// connections alive for 4 seconds, which is too short for a CLI where the +// user may spend 10–30 seconds reading output before typing the next prompt. +// With this custom Agent we get full control over idle connection lifetime. +const KEEP_ALIVE_MSEC = 60_000; // 1 minute + +function createCustomFetch(keepAliveMsecs: number = KEEP_ALIVE_MSEC) { + const agent = new https.Agent({ keepAlive: true, keepAliveMsecs }); + + return async function customFetch(url: string | URL | Request, init?: RequestInit): Promise { + const urlObj = typeof url === "string" ? new URL(url) : url instanceof URL ? url : new URL(url.url); + const { method = "GET", headers = {}, body: reqBody, signal } = init ?? {}; + + // Normalize Headers to a plain Record + const plainHeaders: Record = {}; + if (headers instanceof Headers) { + for (const [k, v] of headers) plainHeaders[k] = v; + } else if (Array.isArray(headers)) { + for (const [k, v] of headers) plainHeaders[k] = v; + } else { + Object.assign(plainHeaders, headers); + } + + const port = urlObj.port ? Number(urlObj.port) : 443; + + return new Promise((resolve, reject) => { + const req = https.request( + { + hostname: urlObj.hostname, + port, + path: urlObj.pathname + urlObj.search, + method, + headers: plainHeaders, + agent, + signal: signal ?? undefined, + }, + (res) => { + const resHeaders = new Headers(); + for (const [k, v] of Object.entries(res.headers)) { + if (v) (Array.isArray(v) ? v : [v]).forEach((val) => resHeaders.append(k, val)); + } + + const body = new ReadableStream({ + start(controller) { + res.on("data", (chunk: Buffer) => controller.enqueue(new Uint8Array(chunk))); + res.on("end", () => controller.close()); + res.on("error", (err) => controller.error(err)); + }, + cancel() { + res.destroy(); + }, + }); + + resolve( + new Response(body, { + status: res.statusCode, + statusText: res.statusMessage, + headers: resHeaders, + }) + ); + } + ); + + req.on("error", reject); + + if (reqBody) { + if (typeof reqBody === "string" || reqBody instanceof Uint8Array || ArrayBuffer.isView(reqBody)) { + req.write(reqBody as Parameters[0]); + } else if (reqBody instanceof ReadableStream) { + // Pipe streaming request body (used for file uploads by the SDK) + const reader = (reqBody as ReadableStream).getReader(); + (async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (value) req.write(value); + } + req.end(); + } catch (err) { + req.destroy(err instanceof Error ? err : new Error(String(err))); + } + })(); + return; // req.end() is called inside the async IIFE + } + } + + req.end(); + }); + }; +} + // Module-level cache for the OpenAI client instance. The client itself is // a stateless fetch wrapper, so it is safe to share across calls as long as // the apiKey + baseURL stay the same. Model, thinking-mode and other @@ -782,6 +876,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { _cachedOpenAI = new OpenAI({ apiKey: settings.apiKey, baseURL: settings.baseURL || undefined, + fetch: createCustomFetch(), }); _cachedOpenAIKey = cacheKey; From 6f8d2e228d853f8014741c8108f6936e7d037c82 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 17:04:09 +0800 Subject: [PATCH 021/212] refactor: replace custom fetch wrapper with undici Agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use npm undici's Agent with keepAliveTimeout: 60s instead of the 90-line custom https.Agent-based fetch wrapper. The approach is the same but much simpler — just pass undiciFetch with a configured Agent dispatcher to the OpenAI SDK. --- src/ui/App.tsx | 103 +++++-------------------------------------------- 1 file changed, 9 insertions(+), 94 deletions(-) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 1cdd8557..42397a54 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -2,10 +2,10 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; import * as fs from "fs"; -import https from "node:https"; import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; +import { Agent, fetch as undiciFetch } from "undici"; import { type LlmStreamProgress, type MessageMeta, @@ -729,98 +729,12 @@ export function resolveCurrentSettings(projectRoot: string = process.cwd()): Res ); } -// Custom fetch implementation that uses node:https.Agent with a configurable -// keepAlive timeout. The default undici-based global fetch only keeps -// connections alive for 4 seconds, which is too short for a CLI where the -// user may spend 10–30 seconds reading output before typing the next prompt. -// With this custom Agent we get full control over idle connection lifetime. -const KEEP_ALIVE_MSEC = 60_000; // 1 minute - -function createCustomFetch(keepAliveMsecs: number = KEEP_ALIVE_MSEC) { - const agent = new https.Agent({ keepAlive: true, keepAliveMsecs }); - - return async function customFetch(url: string | URL | Request, init?: RequestInit): Promise { - const urlObj = typeof url === "string" ? new URL(url) : url instanceof URL ? url : new URL(url.url); - const { method = "GET", headers = {}, body: reqBody, signal } = init ?? {}; - - // Normalize Headers to a plain Record - const plainHeaders: Record = {}; - if (headers instanceof Headers) { - for (const [k, v] of headers) plainHeaders[k] = v; - } else if (Array.isArray(headers)) { - for (const [k, v] of headers) plainHeaders[k] = v; - } else { - Object.assign(plainHeaders, headers); - } - - const port = urlObj.port ? Number(urlObj.port) : 443; - - return new Promise((resolve, reject) => { - const req = https.request( - { - hostname: urlObj.hostname, - port, - path: urlObj.pathname + urlObj.search, - method, - headers: plainHeaders, - agent, - signal: signal ?? undefined, - }, - (res) => { - const resHeaders = new Headers(); - for (const [k, v] of Object.entries(res.headers)) { - if (v) (Array.isArray(v) ? v : [v]).forEach((val) => resHeaders.append(k, val)); - } - - const body = new ReadableStream({ - start(controller) { - res.on("data", (chunk: Buffer) => controller.enqueue(new Uint8Array(chunk))); - res.on("end", () => controller.close()); - res.on("error", (err) => controller.error(err)); - }, - cancel() { - res.destroy(); - }, - }); - - resolve( - new Response(body, { - status: res.statusCode, - statusText: res.statusMessage, - headers: resHeaders, - }) - ); - } - ); - - req.on("error", reject); - - if (reqBody) { - if (typeof reqBody === "string" || reqBody instanceof Uint8Array || ArrayBuffer.isView(reqBody)) { - req.write(reqBody as Parameters[0]); - } else if (reqBody instanceof ReadableStream) { - // Pipe streaming request body (used for file uploads by the SDK) - const reader = (reqBody as ReadableStream).getReader(); - (async () => { - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (value) req.write(value); - } - req.end(); - } catch (err) { - req.destroy(err instanceof Error ? err : new Error(String(err))); - } - })(); - return; // req.end() is called inside the async IIFE - } - } - - req.end(); - }); - }; -} +// Custom undici Agent with a 60-second keepAlive timeout. The default +// global fetch (undici) only keeps connections alive for 4 seconds, which +// is too short for a CLI where the user may spend 10–30 seconds reading +// output between prompts. By passing a dedicated Agent to undiciFetch we +// keep connections reusable for a full minute after the last request. +const _keepAliveAgent = new Agent({ keepAliveTimeout: 60_000 }); // Module-level cache for the OpenAI client instance. The client itself is // a stateless fetch wrapper, so it is safe to share across calls as long as @@ -876,7 +790,8 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { _cachedOpenAI = new OpenAI({ apiKey: settings.apiKey, baseURL: settings.baseURL || undefined, - fetch: createCustomFetch(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: _keepAliveAgent }), }); _cachedOpenAIKey = cacheKey; From 5d48d41b0c46813478f1b055671e9e32181c840f Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 19 May 2026 17:31:52 +0800 Subject: [PATCH 022/212] feat(bash): Add Bash timeout control feature and related adjustments --- src/common/bash-timeout.ts | 12 +++ src/session.ts | 138 +++++++++++++++++++++++++++++--- src/tests/session.test.ts | 34 ++++++++ src/tests/tool-handlers.test.ts | 54 ++++++++++++- src/tools/bash-handler.ts | 106 ++++++++++++++++++++++-- src/tools/executor.ts | 17 ++++ src/ui/App.tsx | 10 ++- src/ui/ProcessStdoutView.tsx | 111 +++++++++++++++++++++---- src/ui/PromptInput.tsx | 4 +- 9 files changed, 450 insertions(+), 36 deletions(-) create mode 100644 src/common/bash-timeout.ts diff --git a/src/common/bash-timeout.ts b/src/common/bash-timeout.ts new file mode 100644 index 00000000..0a76d21a --- /dev/null +++ b/src/common/bash-timeout.ts @@ -0,0 +1,12 @@ +export const DEFAULT_BASH_TIMEOUT_MS = 10 * 60 * 1000; +export const MIN_BASH_TIMEOUT_MS = 60 * 1000; +export const BASH_TIMEOUT_INCREMENT_MS = 5 * 60 * 1000; +export const BASH_TIMEOUT_DECREMENT_MS = 60 * 1000; + +export function clampBashTimeoutMs(timeoutMs: number, minTimeoutMs: number = MIN_BASH_TIMEOUT_MS): number { + if (!Number.isFinite(timeoutMs)) { + return DEFAULT_BASH_TIMEOUT_MS; + } + const minimum = Number.isFinite(minTimeoutMs) ? Math.max(1, Math.round(minTimeoutMs)) : MIN_BASH_TIMEOUT_MS; + return Math.max(minimum, Math.round(timeoutMs)); +} diff --git a/src/session.ts b/src/session.ts index 3b6b67a3..10e0782c 100644 --- a/src/session.ts +++ b/src/session.ts @@ -17,7 +17,12 @@ import { getTools, type ToolDefinition, } from "./prompt"; -import { ToolExecutor, type CreateOpenAIClient } from "./tools/executor"; +import { + ToolExecutor, + type CreateOpenAIClient, + type ProcessTimeoutControl, + type ProcessTimeoutInfo, +} from "./tools/executor"; import { McpManager } from "./mcp/mcp-manager"; import type { McpServerConfig } from "./settings"; import { logApiError } from "./common/error-logger"; @@ -134,6 +139,21 @@ export type ModelUsage = { total_reqs?: number; }; +export type SessionProcessEntry = { + startTime: string; + command: string; + timeoutMs?: number; + deadlineAt?: string; + timedOut?: boolean; +}; + +export type BashTimeoutAdjustment = { + processId: string; + timeoutMs: number; + deadlineAt: string; + timedOut: boolean; +}; + export type SessionEntry = { id: string; summary: string | null; @@ -148,7 +168,7 @@ export type SessionEntry = { activeTokens: number; createTime: string; updateTime: string; - processes: Map | null; // {pid: {startTime, command}} + processes: Map | null; // {pid: process info} }; export type SessionsIndex = { @@ -234,6 +254,7 @@ export class SessionManager { private activeSessionId: string | null = null; private activePromptController: AbortController | null = null; private readonly sessionControllers = new Map(); + private readonly processTimeoutControls = new Map(); private readonly toolExecutor: ToolExecutor; private readonly mcpManager = new McpManager(); private mcpToolDefinitions: ToolDefinition[] = []; @@ -1360,6 +1381,7 @@ ${skillMd} const killedPids: number[] = []; const failedPids: number[] = []; for (const pid of processIds) { + this.processTimeoutControls.delete(this.getProcessControlKey(sessionId, pid)); if (killProcessTree(pid, "SIGKILL")) { killedPids.push(pid); continue; @@ -1397,6 +1419,37 @@ ${skillMd} return !this.sessionControllers.has(sessionId); } + adjustActiveBashTimeout(deltaMs: number): BashTimeoutAdjustment | null { + const sessionId = this.activeSessionId; + if (!sessionId || !Number.isFinite(deltaMs)) { + return null; + } + const session = this.getSession(sessionId); + if (!session?.processes) { + return null; + } + + let selectedPid: string | null = null; + for (const pid of session.processes.keys()) { + if (this.processTimeoutControls.has(this.getProcessControlKey(sessionId, pid))) { + selectedPid = pid; + } + } + if (!selectedPid) { + return null; + } + + const control = this.processTimeoutControls.get(this.getProcessControlKey(sessionId, selectedPid)); + if (!control) { + return null; + } + + const current = control.getInfo(); + const next = control.setTimeoutMs(current.timeoutMs + deltaMs); + this.updateSessionProcessTimeout(sessionId, selectedPid, next); + return this.buildBashTimeoutAdjustment(selectedPid, next); + } + listSessions(): SessionEntry[] { const index = this.loadSessionsIndex(); return index.entries; @@ -1741,6 +1794,7 @@ ${skillMd} onProcessStart: (pid, command) => this.addSessionProcess(sessionId, pid, command), onProcessExit: (pid) => this.removeSessionProcess(sessionId, pid), onProcessStdout: (pid, chunk) => this.onProcessStdout?.(Number(pid), chunk), + onProcessTimeoutControl: (pid, control) => this.setSessionProcessTimeoutControl(sessionId, pid, control), shouldStop: () => this.isInterrupted(sessionId), }); if (this.isInterrupted(sessionId)) { @@ -2137,6 +2191,7 @@ ${skillMd} private removeSessionProcess(sessionId: string, processId: string | number): void { const now = new Date().toISOString(); + this.processTimeoutControls.delete(this.getProcessControlKey(sessionId, processId)); this.updateSessionEntry(sessionId, (entry) => { const processes = new Map(entry.processes ?? []); processes.delete(String(processId)); @@ -2148,7 +2203,58 @@ ${skillMd} }); } - private getProcessIds(processes: Map | null): number[] { + private setSessionProcessTimeoutControl( + sessionId: string, + processId: string | number, + control: ProcessTimeoutControl | null + ): void { + const key = this.getProcessControlKey(sessionId, processId); + if (!control) { + this.processTimeoutControls.delete(key); + return; + } + + this.processTimeoutControls.set(key, control); + this.updateSessionProcessTimeout(sessionId, processId, control.getInfo()); + } + + private updateSessionProcessTimeout(sessionId: string, processId: string | number, info: ProcessTimeoutInfo): void { + const now = new Date().toISOString(); + this.updateSessionEntry(sessionId, (entry) => { + const processes = new Map(entry.processes ?? []); + const pid = String(processId); + const processInfo = processes.get(pid); + if (!processInfo) { + return entry; + } + processes.set(pid, { + ...processInfo, + timeoutMs: info.timeoutMs, + deadlineAt: new Date(info.deadlineAtMs).toISOString(), + timedOut: info.timedOut, + }); + return { + ...entry, + processes, + updateTime: now, + }; + }); + } + + private buildBashTimeoutAdjustment(processId: string, info: ProcessTimeoutInfo): BashTimeoutAdjustment { + return { + processId, + timeoutMs: info.timeoutMs, + deadlineAt: new Date(info.deadlineAtMs).toISOString(), + timedOut: info.timedOut, + }; + } + + private getProcessControlKey(sessionId: string, processId: string | number): string { + return `${sessionId}:${String(processId)}`; + } + + private getProcessIds(processes: Map | null): number[] { if (!processes) { return []; } @@ -2232,11 +2338,11 @@ ${skillMd} return usagePerModel; } - private deserializeProcesses(value: unknown): Map | null { + private deserializeProcesses(value: unknown): Map | null { if (!value || typeof value !== "object") { return null; } - const processes = new Map(); + const processes = new Map(); for (const [pid, entry] of Object.entries(value as Record)) { if (!pid) { continue; @@ -2245,22 +2351,34 @@ ${skillMd} // Backward compatibility for old format where just stored start time processes.set(pid, { startTime: entry, command: "Running process..." }); } else if (typeof entry === "object" && entry !== null) { - const obj = entry as { startTime?: unknown; command?: unknown }; + const obj = entry as { + startTime?: unknown; + command?: unknown; + timeoutMs?: unknown; + deadlineAt?: unknown; + timedOut?: unknown; + }; const startTime = typeof obj.startTime === "string" ? obj.startTime : new Date().toISOString(); const command = typeof obj.command === "string" ? obj.command : "Running process..."; - processes.set(pid, { startTime, command }); + processes.set(pid, { + startTime, + command, + timeoutMs: typeof obj.timeoutMs === "number" ? obj.timeoutMs : undefined, + deadlineAt: typeof obj.deadlineAt === "string" ? obj.deadlineAt : undefined, + timedOut: typeof obj.timedOut === "boolean" ? obj.timedOut : undefined, + }); } } return processes.size > 0 ? processes : null; } private serializeProcesses( - processes: Map | null - ): Record | null { + processes: Map | null + ): Record | null { if (!processes || processes.size === 0) { return null; } - const serialized: Record = {}; + const serialized: Record = {}; for (const [pid, entry] of processes.entries()) { serialized[pid] = entry; } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index b7eadaeb..2ab4fe99 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1641,6 +1641,40 @@ test("SessionManager treats OpenAI APIUserAbortError as interrupted", async () = assert.equal(session?.failReason, "interrupted"); }); +test("SessionManager adjusts the active Bash timeout control and session metadata", async () => { + const workspace = createTempDir("deepcode-bash-timeout-session-"); + const manager = createSessionManager(workspace, ""); + const sessionId = await manager.createSession({ text: "hello" }); + + (manager as any).addSessionProcess(sessionId, 123, "sleep 10"); + + let timeoutInfo = { + timeoutMs: 10 * 60 * 1000, + startedAtMs: 1000, + deadlineAtMs: 1000 + 10 * 60 * 1000, + timedOut: false, + }; + (manager as any).setSessionProcessTimeoutControl(sessionId, 123, { + getInfo: () => timeoutInfo, + setTimeoutMs: (timeoutMs: number) => { + timeoutInfo = { + ...timeoutInfo, + timeoutMs, + deadlineAtMs: timeoutInfo.startedAtMs + timeoutMs, + }; + return timeoutInfo; + }, + }); + + const adjustment = manager.adjustActiveBashTimeout(5 * 60 * 1000); + const processInfo = manager.getSession(sessionId)?.processes?.get("123"); + + assert.equal(adjustment?.processId, "123"); + assert.equal(adjustment?.timeoutMs, 15 * 60 * 1000); + assert.equal(processInfo?.timeoutMs, 15 * 60 * 1000); + assert.equal(processInfo?.deadlineAt, new Date(timeoutInfo.deadlineAtMs).toISOString()); +}); + function createSessionManager(projectRoot: string, machineId: string): SessionManager { return new SessionManager({ projectRoot, diff --git a/src/tests/tool-handlers.test.ts b/src/tests/tool-handlers.test.ts index 0b21eddb..f66153c5 100644 --- a/src/tests/tool-handlers.test.ts +++ b/src/tests/tool-handlers.test.ts @@ -4,7 +4,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { setTimeout as delay } from "node:timers/promises"; -import type { ToolExecutionContext } from "../tools/executor"; +import type { ProcessTimeoutControl, ToolExecutionContext } from "../tools/executor"; import { handleBashTool } from "../tools/bash-handler"; import { handleEditTool } from "../tools/edit-handler"; import { handleReadTool } from "../tools/read-handler"; @@ -52,6 +52,58 @@ test("Bash streams stdout and stderr before command completion", async () => { assert.match(streamedOutput, /err/); }); +test("Bash terminates commands that exceed the configured timeout", async () => { + const workspace = createTempWorkspace(); + const exitedPids: Array = []; + + const result = await handleBashTool( + { + command: "printf 'start\\n'; sleep 5; printf 'done\\n'", + }, + createContext("bash-timeout", workspace, { + bashTimeoutMs: 100, + bashMinTimeoutMs: 1, + onProcessExit: (pid) => { + exitedPids.push(pid); + }, + }) + ); + + assert.equal(result.ok, false); + assert.equal(result.error, "Command timed out."); + assert.equal(result.metadata?.timedOut, true); + assert.equal(result.metadata?.timeoutMs, 100); + assert.doesNotMatch(result.output ?? "", /done/); + assert.equal(exitedPids.length, 1); +}); + +test("Bash timeout control can extend the active command deadline", async () => { + const workspace = createTempWorkspace(); + let timeoutControl: ProcessTimeoutControl | null = null; + + const result = await handleBashTool( + { + command: "sleep 0.2; printf 'done\\n'", + }, + createContext("bash-timeout-extend", workspace, { + bashTimeoutMs: 100, + bashMinTimeoutMs: 1, + onProcessTimeoutControl: (_pid, control) => { + if (control) { + timeoutControl = control; + control.setTimeoutMs(1000); + } + }, + }) + ); + + assert.ok(timeoutControl); + assert.equal(result.ok, true); + assert.match(result.output ?? "", /done/); + assert.equal(result.metadata?.timedOut, false); + assert.equal(result.metadata?.timeoutMs, 1000); +}); + test("UpdatePlan accepts a markdown task list string", async () => { const workspace = createTempWorkspace(); const plan = ["## Task List", "", "- [>] Inspect current behavior", "- [ ] Implement UpdatePlan"].join("\n"); diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts index 071da530..42722710 100644 --- a/src/tools/bash-handler.ts +++ b/src/tools/bash-handler.ts @@ -1,5 +1,7 @@ import { spawn } from "child_process"; -import type { ToolExecutionContext, ToolExecutionResult } from "./executor"; +import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/bash-timeout"; +import { killProcessTree } from "../common/process-tree"; +import type { ProcessTimeoutControl, ProcessTimeoutInfo, ToolExecutionContext, ToolExecutionResult } from "./executor"; import { buildDisableExtglobCommand, buildShellEnv, @@ -22,6 +24,9 @@ type ToolCommandResult = { truncated: boolean; shellPath?: string; startCwd?: string; + timedOut?: boolean; + timeoutMs?: number; + deadlineAt?: string; }; export async function handleBashTool( @@ -48,12 +53,15 @@ export async function handleBashTool( execution.exitCode, execution.signal, shellPath, - startCwd + startCwd, + execution.timedOut, + execution.timeoutMs, + execution.deadlineAtMs ); updateSessionCwd(context.sessionId, startCwd, result.cwd); if (execution.error || result.exitCode !== 0 || result.signal !== null) { - const errorMessage = buildErrorMessage(result.exitCode, result.signal, execution.error); + const errorMessage = buildErrorMessage(result.exitCode, result.signal, execution.error, execution.timedOut); return formatResult({ ...result, ok: false }, "bash", errorMessage); } @@ -102,10 +110,27 @@ async function executeShellCommand( cwd: string, command: string, context: ToolExecutionContext -): Promise<{ stdout: string; stderr: string; exitCode: number | null; signal: string | null; error?: string }> { +): Promise<{ + stdout: string; + stderr: string; + exitCode: number | null; + signal: string | null; + error?: string; + timedOut: boolean; + timeoutMs: number; + deadlineAtMs: number; +}> { return new Promise((resolve) => { const detached = process.platform !== "win32"; const configuredEnv = context.createOpenAIClient?.().env ?? {}; + const minTimeoutMs = context.bashMinTimeoutMs; + const initialTimeoutMs = clampBashTimeoutMs(context.bashTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS, minTimeoutMs); + const startedAtMs = Date.now(); + let timeoutMs = initialTimeoutMs; + let deadlineAtMs = startedAtMs + timeoutMs; + let timedOut = false; + let settled = false; + let timeoutTimer: ReturnType | null = null; const child = spawn(shellPath, shellArgs, { cwd, env: buildShellEnv(shellPath, configuredEnv), @@ -114,8 +139,53 @@ async function executeShellCommand( stdio: ["ignore", "pipe", "pipe"], }); const pid = child.pid; + + const getTimeoutInfo = (): ProcessTimeoutInfo => ({ + timeoutMs, + startedAtMs, + deadlineAtMs, + timedOut, + }); + const stopTimeoutTimer = () => { + if (timeoutTimer) { + clearTimeout(timeoutTimer); + timeoutTimer = null; + } + }; + const triggerTimeout = () => { + if (settled || timedOut || typeof pid !== "number") { + return; + } + timedOut = true; + stopTimeoutTimer(); + killProcessTree(pid, "SIGKILL"); + }; + const scheduleTimeout = () => { + stopTimeoutTimer(); + if (settled) { + return; + } + const remainingMs = Math.max(0, deadlineAtMs - Date.now()); + timeoutTimer = setTimeout(triggerTimeout, remainingMs); + }; + const timeoutControl: ProcessTimeoutControl = { + getInfo: getTimeoutInfo, + setTimeoutMs: (nextTimeoutMs) => { + timeoutMs = clampBashTimeoutMs(nextTimeoutMs, minTimeoutMs); + deadlineAtMs = startedAtMs + timeoutMs; + if (deadlineAtMs <= Date.now()) { + triggerTimeout(); + } else { + scheduleTimeout(); + } + return getTimeoutInfo(); + }, + }; + if (typeof pid === "number") { context.onProcessStart?.(pid, command); + context.onProcessTimeoutControl?.(pid, timeoutControl); + scheduleTimeout(); } let stdout = ""; @@ -138,7 +208,10 @@ async function executeShellCommand( }); child.on("close", (code, signal) => { + settled = true; + stopTimeoutTimer(); if (typeof pid === "number") { + context.onProcessTimeoutControl?.(pid, null); context.onProcessExit?.(pid); } resolve({ @@ -147,6 +220,9 @@ async function executeShellCommand( exitCode: typeof code === "number" ? code : null, signal: signal ?? null, error, + timedOut, + timeoutMs, + deadlineAtMs, }); }); }); @@ -173,7 +249,10 @@ function buildToolCommandResult( exitCode: number | null, signal: string | null, shellPath: string, - startCwd: string + startCwd: string, + timedOut: boolean = false, + timeoutMs?: number, + deadlineAtMs?: number ): ToolCommandResult { const { output: cleanedStdout, cwd } = stripMarker(stdout, marker); const combined = joinOutput(cleanedStdout, stderr); @@ -187,6 +266,9 @@ function buildToolCommandResult( truncated, shellPath, startCwd, + timedOut, + timeoutMs, + deadlineAt: typeof deadlineAtMs === "number" ? new Date(deadlineAtMs).toISOString() : undefined, }; } @@ -231,10 +313,13 @@ function truncateOutput(output: string): { text: string; truncated: boolean } { return { text: output.slice(0, MAX_OUTPUT_CHARS), truncated: true }; } -function buildErrorMessage(exitCode: number | null, signal: string | null, error?: string): string { +function buildErrorMessage(exitCode: number | null, signal: string | null, error?: string, timedOut = false): string { if (error) { return error; } + if (timedOut) { + return "Command timed out."; + } if (signal) { return `Command terminated by signal ${signal}.`; } @@ -253,6 +338,15 @@ function formatResult(result: ToolCommandResult, name: string, errorMessage?: st shellPath: result.shellPath, startCwd: result.startCwd, }; + if (typeof result.timedOut === "boolean") { + metadata.timedOut = result.timedOut; + } + if (typeof result.timeoutMs === "number") { + metadata.timeoutMs = result.timeoutMs; + } + if (result.deadlineAt) { + metadata.deadlineAt = result.deadlineAt; + } const outputValue = result.output ? result.output : undefined; diff --git a/src/tools/executor.ts b/src/tools/executor.ts index 70ceab13..093e9f3b 100644 --- a/src/tools/executor.ts +++ b/src/tools/executor.ts @@ -39,15 +39,31 @@ export type ToolExecutionContext = { onProcessStart?: (processId: string | number, command: string) => void; onProcessExit?: (processId: string | number) => void; onProcessStdout?: (processId: string | number, chunk: string) => void; + onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; + bashTimeoutMs?: number; + bashMinTimeoutMs?: number; }; export type ToolExecutionHooks = { onProcessStart?: (processId: string | number, command: string) => void; onProcessExit?: (processId: string | number) => void; onProcessStdout?: (processId: string | number, chunk: string) => void; + onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; shouldStop?: () => boolean; }; +export type ProcessTimeoutInfo = { + timeoutMs: number; + startedAtMs: number; + deadlineAtMs: number; + timedOut: boolean; +}; + +export type ProcessTimeoutControl = { + getInfo: () => ProcessTimeoutInfo; + setTimeoutMs: (timeoutMs: number) => ProcessTimeoutInfo; +}; + export type ToolExecutionResult = { ok: boolean; name: string; @@ -200,6 +216,7 @@ export class ToolExecutor { onProcessStart: hooks?.onProcessStart, onProcessExit: hooks?.onProcessExit, onProcessStdout: hooks?.onProcessStdout, + onProcessTimeoutControl: hooks?.onProcessTimeoutControl, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 582abaff..c729dc0e 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -54,7 +54,7 @@ type AppProps = { export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement { const { exit } = useApp(); const { stdout, write } = useStdout(); - const { columns } = useWindowSize(); + const { columns, rows } = useWindowSize(); const { mode, setMode } = useRawModeContext(); const initialPromptSubmittedRef = useRef(false); const processStdoutRef = useRef>(new Map()); @@ -281,6 +281,11 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setShowProcessStdout(false); }, []); + const handleAdjustBashTimeout = useCallback( + (deltaMs: number) => sessionManager.adjustActiveBashTimeout(deltaMs), + [sessionManager] + ); + const handleModelConfigChange = useCallback( (selection: ModelConfigSelection): string => { const current = resolveCurrentSettings(projectRoot); @@ -467,6 +472,7 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. }, [busy, mode, sessionManager, columns, stdout]); const screenWidth = useMemo(() => columns ?? stdout?.columns ?? 80, [columns, stdout]); + const screenHeight = useMemo(() => rows ?? stdout?.rows ?? 24, [rows, stdout]); const promptHistory = useMemo(() => { return messages .filter((message) => message.role === "user" && typeof message.content === "string") @@ -568,7 +574,9 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. processStdoutRef={processStdoutRef} runningProcesses={runningProcesses} onDismiss={handleDismissProcessStdout} + onAdjustTimeout={handleAdjustBashTimeout} screenWidth={screenWidth} + screenHeight={screenHeight} /> ) : view === "session-list" ? ( >; runningProcesses: RunningProcesses; onDismiss: () => void; + onAdjustTimeout: (deltaMs: number) => BashTimeoutAdjustment | null; screenWidth: number; + screenHeight: number; }; const REFRESH_INTERVAL_MS = 150; -const MAX_VISIBLE_LINES = 100; +const MAX_PANEL_HEIGHT = 30; +const MIN_PANEL_HEIGHT = 5; export const ProcessStdoutView = React.memo(function ProcessStdoutView({ processStdoutRef, runningProcesses, onDismiss, + onAdjustTimeout, screenWidth, + screenHeight, }: ProcessStdoutViewProps): React.ReactElement { const [stdoutText, setStdoutText] = useState(""); const [scrollOffset, setScrollOffset] = useState(0); - const containerRef = useRef<{ lineCount: number }>({ lineCount: 0 }); + const [statusMessage, setStatusMessage] = useState(""); + const statusTimerRef = useRef | null>(null); + + const panelHeight = Math.max(MIN_PANEL_HEIGHT, Math.min(screenHeight - 1, MAX_PANEL_HEIGHT)); + const reservedRows = statusMessage ? 2 : 1; + const visibleLineLimit = Math.max(1, panelHeight - reservedRows); useEffect(() => { const updateStdout = () => { @@ -51,21 +62,37 @@ export const ProcessStdoutView = React.memo(function ProcessStdoutView({ return () => clearInterval(interval); }, [processStdoutRef, runningProcesses]); - // Update container line count for scroll awareness + useEffect(() => { + return () => { + if (statusTimerRef.current) { + clearTimeout(statusTimerRef.current); + } + }; + }, []); + const lines = useMemo(() => stdoutText.split("\n"), [stdoutText]); - containerRef.current.lineCount = lines.length; + const timeoutProcess = useMemo(() => getLatestTimeoutProcess(runningProcesses), [runningProcesses]); const visibleLines = useMemo(() => { - if (lines.length <= MAX_VISIBLE_LINES) { + if (lines.length <= visibleLineLimit) { return lines; } - const start = Math.max(0, lines.length - MAX_VISIBLE_LINES - scrollOffset); - const slice = lines.slice(start, start + MAX_VISIBLE_LINES); - if (lines.length > MAX_VISIBLE_LINES) { + const outputLineLimit = Math.max(1, visibleLineLimit - 1); + const start = Math.max(0, lines.length - outputLineLimit - scrollOffset); + const slice = lines.slice(start, start + outputLineLimit); + if (lines.length > visibleLineLimit) { slice.unshift(`... (${start} lines above · ↑/↓ to scroll · ${lines.length} total lines) ...`); } return slice; - }, [lines, scrollOffset]); + }, [lines, scrollOffset, visibleLineLimit]); + + const setTemporaryStatus = (message: string) => { + setStatusMessage(message); + if (statusTimerRef.current) { + clearTimeout(statusTimerRef.current); + } + statusTimerRef.current = setTimeout(() => setStatusMessage(""), 2000); + }; useTerminalInput( (input, key) => { @@ -73,8 +100,18 @@ export const ProcessStdoutView = React.memo(function ProcessStdoutView({ onDismiss(); return; } + if (input === "+") { + const adjustment = onAdjustTimeout(BASH_TIMEOUT_INCREMENT_MS); + setTemporaryStatus(formatAdjustmentStatus(adjustment)); + return; + } + if (input === "-") { + const adjustment = onAdjustTimeout(-BASH_TIMEOUT_DECREMENT_MS); + setTemporaryStatus(formatAdjustmentStatus(adjustment)); + return; + } if (key.upArrow) { - setScrollOffset((s) => Math.min(s + 10, Math.max(0, lines.length - MAX_VISIBLE_LINES))); + setScrollOffset((s) => Math.min(s + 10, Math.max(0, lines.length - visibleLineLimit))); return; } if (key.downArrow) { @@ -82,11 +119,11 @@ export const ProcessStdoutView = React.memo(function ProcessStdoutView({ return; } if (key.pageUp) { - setScrollOffset((s) => Math.min(s + MAX_VISIBLE_LINES, Math.max(0, lines.length - MAX_VISIBLE_LINES))); + setScrollOffset((s) => Math.min(s + visibleLineLimit, Math.max(0, lines.length - visibleLineLimit))); return; } if (key.pageDown) { - setScrollOffset((s) => Math.max(s - MAX_VISIBLE_LINES, 0)); + setScrollOffset((s) => Math.max(s - visibleLineLimit, 0)); return; } }, @@ -94,16 +131,58 @@ export const ProcessStdoutView = React.memo(function ProcessStdoutView({ ); return ( - + 📟 Process Output - (Ctrl+O or Esc to close · ↑↓ PageUp/PageDown to scroll) + {` (${formatTimeoutHint( + timeoutProcess?.entry + )} · +/- adjust · Ctrl+O or Esc to close · ↑↓ PageUp/PageDown to scroll)`} - + {visibleLines.map((line, index) => ( {line} ))} + {statusMessage ? ( + + {statusMessage} + + ) : null} ); }); + +function getLatestTimeoutProcess( + runningProcesses: RunningProcesses +): { pid: string; entry: SessionProcessEntry } | null { + if (!runningProcesses) { + return null; + } + let latest: { pid: string; entry: SessionProcessEntry } | null = null; + for (const [pid, entry] of runningProcesses.entries()) { + if (typeof entry.timeoutMs !== "number") { + continue; + } + latest = { pid, entry }; + } + return latest; +} + +function formatTimeoutHint(entry?: SessionProcessEntry): string { + if (!entry || typeof entry.timeoutMs !== "number") { + return "timeout unavailable"; + } + return `timeout ${formatDuration(entry.timeoutMs)}`; +} + +function formatAdjustmentStatus(adjustment: BashTimeoutAdjustment | null): string { + if (!adjustment) { + return "No adjustable Bash timeout"; + } + return `Timeout set to ${formatDuration(adjustment.timeoutMs)}`; +} + +function formatDuration(ms: number): string { + const totalMinutes = Math.max(1, Math.round(ms / 60000)); + return `${totalMinutes}m`; +} diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 1096a936..b35f72e9 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -39,7 +39,7 @@ import { } from "./fileMentions"; import type { FileMentionItem } from "./fileMentions"; import { readClipboardImageAsync } from "./clipboard"; -import type { SkillInfo } from "../session"; +import type { SessionEntry, SkillInfo } from "../session"; // Re-exported from prompt modules for backward compatibility export { useTerminalInput, parseTerminalInput } from "./prompt"; @@ -70,7 +70,7 @@ type Props = { loadingText?: string | null; disabled?: boolean; placeholder?: string; - runningProcesses?: Map | null; + runningProcesses?: SessionEntry["processes"]; onSubmit: (submission: PromptSubmission) => void; onModelConfigChange: (selection: ModelConfigSelection) => string | Promise; onRawModeChange?: (mode: string) => void; From c081efd169a7c2c47a503eb540b538640ac8810a Mon Sep 17 00:00:00 2001 From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com> Date: Tue, 19 May 2026 18:16:19 +0800 Subject: [PATCH 023/212] Revert "fix: re-apply dynamic modifier parsing for Shift+Enter after upstream sync" This reverts commit 52dafba25903dc70258d7e59dbe86e283a0f091f. --- src/tests/promptInputKeys.test.ts | 6 ++--- src/ui/prompt/cursor.ts | 4 +-- src/ui/prompt/useTerminalInput.ts | 43 +++---------------------------- 3 files changed, 9 insertions(+), 44 deletions(-) diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts index 8952a3d9..69d20758 100644 --- a/src/tests/promptInputKeys.test.ts +++ b/src/tests/promptInputKeys.test.ts @@ -80,7 +80,7 @@ test("parseTerminalInput keeps BS payload for meta+backspace", () => { test("parseTerminalInput recognizes shifted return sequences", () => { const { input, key } = parseTerminalInput("\u001B\r"); - assert.equal(input, ""); + assert.equal(input, "\r"); assert.equal(key.return, true); assert.equal(key.shift, true); assert.equal(key.meta, false); @@ -108,8 +108,8 @@ test("parseTerminalInput recognizes alternate shifted return sequences", () => { }); test("terminal extended key helpers request and restore modifyOtherKeys mode", () => { - assert.equal(enableTerminalExtendedKeys(), "\u001B[>4;1m\u001B[>1u"); - assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m\u001B[4;1m"); + assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m"); }); test("parseTerminalInput recognizes terminal focus events", () => { diff --git a/src/ui/prompt/cursor.ts b/src/ui/prompt/cursor.ts index 59b24f23..2668470c 100644 --- a/src/ui/prompt/cursor.ts +++ b/src/ui/prompt/cursor.ts @@ -41,11 +41,11 @@ function disableTerminalFocusReporting(): string { } export function enableTerminalExtendedKeys(): string { - return "\u001B[>4;1m\u001B[>1u"; + return "\u001B[>4;1m"; } export function disableTerminalExtendedKeys(): string { - return "\u001B[>4;0m\u001B[4;0m"; } export function getPromptCursorPlacement( diff --git a/src/ui/prompt/useTerminalInput.ts b/src/ui/prompt/useTerminalInput.ts index f448d4fc..8013ff60 100644 --- a/src/ui/prompt/useTerminalInput.ts +++ b/src/ui/prompt/useTerminalInput.ts @@ -26,42 +26,7 @@ const BACKSPACE_BYTES = new Set(["\u007F", "\b"]); const FORWARD_DELETE_SEQUENCES = new Set(["\u001B[3~", "\u001B[P"]); const HOME_SEQUENCES = new Set(["\u001B[H", "\u001B[1~", "\u001B[7~", "\u001BOH"]); const END_SEQUENCES = new Set(["\u001B[F", "\u001B[4~", "\u001B[8~", "\u001BOF"]); -const SHIFT_RETURN_SEQUENCES = new Set([ - "\u001B\r", - "\u001B[13;2u", - "\u001B[13;1u", - "\u001B[13;2~", - "\u001B[13;1~", - "\u001B[27;2;13~", - "\u001B[27;1;13~", -]); - -const CSI_SHIFT_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/; -const CSI_EXTENDED_SHIFT_RETURN_RE = /^\u001B\[27;(\d+);13~$/; - -function isShiftReturn(raw: string): boolean { - if (SHIFT_RETURN_SEQUENCES.has(raw)) return true; - let m: RegExpMatchArray | null; - if ((m = raw.match(CSI_SHIFT_RETURN_RE)) !== null) { - const mod = parseInt(m[1], 10); - return (mod & 2) !== 0 || (mod & 1) !== 0; - } - if ((m = raw.match(CSI_EXTENDED_SHIFT_RETURN_RE)) !== null) { - const mod = parseInt(m[1], 10); - return (mod & 2) !== 0 || (mod & 1) !== 0; - } - return false; -} - -const CSI_RETURN_RE = /^\u001B\[13;(\d+)[u~]$/; -const CSI_EXTENDED_RETURN_RE = /^\u001B\[27;(\d+);13~$/; - -function isReturn(raw: string): boolean { - if (raw === "\r") return true; - if (SHIFT_RETURN_SEQUENCES.has(raw)) return true; - if (META_RETURN_SEQUENCES.has(raw)) return true; - return CSI_RETURN_RE.test(raw) || CSI_EXTENDED_RETURN_RE.test(raw); -} +const SHIFT_RETURN_SEQUENCES = new Set(["\u001B\r", "\u001B[13;2u", "\u001B[13;2~", "\u001B[27;2;13~"]); const META_RETURN_SEQUENCES = new Set(["\u001B[13;3u", "\u001B[13;4u"]); const CTRL_LEFT_SEQUENCES = new Set(["\u001B[1;5D", "\u001B[5D"]); const CTRL_RIGHT_SEQUENCES = new Set(["\u001B[1;5C", "\u001B[5C"]); @@ -148,10 +113,10 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key: end: END_SEQUENCES.has(raw), pageDown: raw === "\u001B[6~", pageUp: raw === "\u001B[5~", - return: isReturn(raw), + return: raw === "\r" || SHIFT_RETURN_SEQUENCES.has(raw) || META_RETURN_SEQUENCES.has(raw), escape: raw === "\u001B", ctrl: CTRL_LEFT_SEQUENCES.has(raw) || CTRL_RIGHT_SEQUENCES.has(raw), - shift: isShiftReturn(raw), + shift: SHIFT_RETURN_SEQUENCES.has(raw), tab: raw === "\t" || raw === "\u001B[Z", backspace: BACKSPACE_BYTES.has(raw), delete: FORWARD_DELETE_SEQUENCES.has(raw), @@ -197,7 +162,7 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key: key.shift = true; } - if (key.tab || key.backspace || key.delete || key.return) { + if (key.tab || key.backspace || key.delete) { input = ""; } From 255226a3c9bdd7254dd8b5728a0e1bff7de28707 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 18:33:10 +0800 Subject: [PATCH 024/212] chore: add undici devDependency for custom keepAlive Agent Required by the custom fetch wrapper that replaces the default 4s keepAlive undici global dispatcher with a custom Agent (60s). --- package-lock.json | 13 ++++++++++++- package.json | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 800d75a5..0b43587f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,8 @@ "prettier": "^3.8.3", "tsx": "^4.21.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2" + "typescript-eslint": "^8.59.2", + "undici": "^8.3.0" }, "engines": { "node": ">=22" @@ -4096,6 +4097,16 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/undici": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", diff --git a/package.json b/package.json index c438d689..b805d18f 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "prettier": "^3.8.3", "tsx": "^4.21.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2" + "typescript-eslint": "^8.59.2", + "undici": "^8.3.0" } } From 5b74c00db5bf16e1519c6aaafb233c4c2b78bf1a Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 18:33:59 +0800 Subject: [PATCH 025/212] fix: move undici from devDependencies to dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit undici is imported at runtime in App.tsx for the custom keepAlive Agent. When bundled with --packages=external, end users need the package installed — it cannot be a devDependency. --- package-lock.json | 5 ++--- package.json | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b43587f..7d68f74c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "ink-gradient": "^4.0.0", "openai": "^6.35.0", "react": "^19.2.5", + "undici": "^8.3.0", "zod": "^4.4.3" }, "bin": { @@ -38,8 +39,7 @@ "prettier": "^3.8.3", "tsx": "^4.21.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2", - "undici": "^8.3.0" + "typescript-eslint": "^8.59.2" }, "engines": { "node": ">=22" @@ -4101,7 +4101,6 @@ "version": "8.3.0", "resolved": "https://registry.npmmirror.com/undici/-/undici-8.3.0.tgz", "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=22.19.0" diff --git a/package.json b/package.json index b805d18f..6d58864f 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "ink-gradient": "^4.0.0", "openai": "^6.35.0", "react": "^19.2.5", + "undici": "^8.3.0", "zod": "^4.4.3" }, "devDependencies": { @@ -65,7 +66,6 @@ "prettier": "^3.8.3", "tsx": "^4.21.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2", - "undici": "^8.3.0" + "typescript-eslint": "^8.59.2" } } From db78e2b1756e2e9e2f9eea008e30e2f4638e0856 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 18:47:23 +0800 Subject: [PATCH 026/212] fix: downgrade undici to v7 for Node 20 compatibility undici v8 requires Node >=22, but the CI matrix includes Node 20 which the project intentionally supports. v7 works on >=20.18.1. --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7d68f74c..82db9af9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "ink-gradient": "^4.0.0", "openai": "^6.35.0", "react": "^19.2.5", - "undici": "^8.3.0", + "undici": "^7.25.0", "zod": "^4.4.3" }, "bin": { @@ -4098,12 +4098,12 @@ } }, "node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmmirror.com/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "version": "7.25.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "license": "MIT", "engines": { - "node": ">=22.19.0" + "node": ">=20.18.1" } }, "node_modules/undici-types": { diff --git a/package.json b/package.json index 6d58864f..b2826c20 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "ink-gradient": "^4.0.0", "openai": "^6.35.0", "react": "^19.2.5", - "undici": "^8.3.0", + "undici": "^7.25.0", "zod": "^4.4.3" }, "devDependencies": { From 87d52ade53833a18118e23a595f511a4823b0a6c Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 19 May 2026 19:00:28 +0800 Subject: [PATCH 027/212] fix: add 3s timeout to warmup request to prevent exit hang Codex review found that the fire-and-forget warmup models.list() had no timeout. The OpenAI client defaults to a 10-minute timeout, so an unreachable API could keep the Node process alive long after the user exits. --- src/ui/App.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 42397a54..515d5e6a 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -796,9 +796,17 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { _cachedOpenAIKey = cacheKey; // Fire-and-forget warmup: pre-establish TCP+TLS connection to the API - // server while the user is composing their first prompt. Errors are - // silently ignored — the real request will retry on its own if needed. - void _cachedOpenAI.models.list().catch(() => {}); + // server while the user is composing their first prompt. Bounded by a + // short timeout so a slow / unreachable API never blocks process exit. + void (async () => { + const ac = new AbortController(); + const timer = setTimeout(() => ac.abort(), 3000); + try { + await _cachedOpenAI.models.list({ signal: ac.signal }).catch(() => {}); + } finally { + clearTimeout(timer); + } + })(); return { client: _cachedOpenAI, From 38246a0192a6c20f7eec6e1d6d904cd5ab335925 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 19 May 2026 19:40:37 +0800 Subject: [PATCH 028/212] 0.1.23 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 800d75a5..17a77cae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.22", + "version": "0.1.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.22", + "version": "0.1.23", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index c438d689..b72fd96a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.22", + "version": "0.1.23", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From f28cbce383fe342e4815f54db084ffec359a8006 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 09:02:30 +0800 Subject: [PATCH 029/212] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E6=AD=A3=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E8=B7=AF=E5=BE=84=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将所有导入路径中的 "compoments" 修正为 "components" - 更新多个文件中相关的导入语句,包括 App.tsx、index.ts、messageView.test.ts 和 PromptInput.tsx - 保证组件引用路径正确,避免运行时找不到模块错误 - 提升代码的可维护性和一致性 --- src/tests/messageView.test.ts | 4 ++-- src/ui/App.tsx | 4 ++-- src/ui/PromptInput.tsx | 2 +- src/ui/{compoments => components}/MessageView/index.tsx | 0 src/ui/{compoments => components}/MessageView/markdown.ts | 0 src/ui/{compoments => components}/MessageView/types.ts | 0 src/ui/{compoments => components}/MessageView/utils.ts | 0 .../{compoments => components}/RawModeExitPrompt/index.tsx | 0 .../{compoments => components}/RawModelDropdown/index.tsx | 0 src/ui/{compoments => components}/index.ts | 0 src/ui/index.ts | 6 +++--- 11 files changed, 8 insertions(+), 8 deletions(-) rename src/ui/{compoments => components}/MessageView/index.tsx (100%) rename src/ui/{compoments => components}/MessageView/markdown.ts (100%) rename src/ui/{compoments => components}/MessageView/types.ts (100%) rename src/ui/{compoments => components}/MessageView/utils.ts (100%) rename src/ui/{compoments => components}/RawModeExitPrompt/index.tsx (100%) rename src/ui/{compoments => components}/RawModelDropdown/index.tsx (100%) rename src/ui/{compoments => components}/index.ts (100%) diff --git a/src/tests/messageView.test.ts b/src/tests/messageView.test.ts index 990c8ff7..b806dbd1 100644 --- a/src/tests/messageView.test.ts +++ b/src/tests/messageView.test.ts @@ -6,10 +6,10 @@ import { renderMessageToStdout, getUpdatePlanPreviewLines, parseToolPayload, -} from "../ui/compoments/MessageView/utils"; +} from "../ui/components/MessageView/utils"; import { RawMode } from "../ui/contexts"; import type { SessionMessage } from "../session"; -import type { ToolSummary } from "../ui/compoments/MessageView/types"; +import type { ToolSummary } from "../ui/components/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { const lines = parseDiffPreview( diff --git a/src/ui/App.tsx b/src/ui/App.tsx index c729dc0e..8ba842ae 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -23,7 +23,7 @@ import { resolveSettingsSources, } from "../settings"; import { PromptInput, type PromptSubmission } from "./PromptInput"; -import { MessageView, RawModeExitPrompt } from "./compoments"; +import { MessageView, RawModeExitPrompt } from "./components"; import { SessionList } from "./SessionList"; import { buildLoadingText } from "./loadingText"; import { findExpandedThinkingId } from "./thinkingState"; @@ -38,7 +38,7 @@ import { } from "./askUserQuestion"; import { buildExitSummaryText } from "./exitSummary"; import { RawMode, useRawModeContext } from "./contexts"; -import { renderMessageToStdout } from "./compoments/MessageView/utils"; +import { renderMessageToStdout } from "./components/MessageView/utils"; const DEFAULT_MODEL = "deepseek-v4-pro"; const DEFAULT_BASE_URL = "https://api.deepseek.com"; diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index b35f72e9..b9b1f8e5 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -51,7 +51,7 @@ import { useHiddenTerminalCursor, useTerminalExtendedKeys, useTerminalFocusRepor import SlashCommandMenu from "./SlashCommandMenu"; import type { ModelConfigSelection, ReasoningEffort } from "../settings"; import DropdownMenu from "./DropdownMenu"; -import { RawModelDropdown } from "./compoments"; +import { RawModelDropdown } from "./components"; export type PromptSubmission = { text: string; diff --git a/src/ui/compoments/MessageView/index.tsx b/src/ui/components/MessageView/index.tsx similarity index 100% rename from src/ui/compoments/MessageView/index.tsx rename to src/ui/components/MessageView/index.tsx diff --git a/src/ui/compoments/MessageView/markdown.ts b/src/ui/components/MessageView/markdown.ts similarity index 100% rename from src/ui/compoments/MessageView/markdown.ts rename to src/ui/components/MessageView/markdown.ts diff --git a/src/ui/compoments/MessageView/types.ts b/src/ui/components/MessageView/types.ts similarity index 100% rename from src/ui/compoments/MessageView/types.ts rename to src/ui/components/MessageView/types.ts diff --git a/src/ui/compoments/MessageView/utils.ts b/src/ui/components/MessageView/utils.ts similarity index 100% rename from src/ui/compoments/MessageView/utils.ts rename to src/ui/components/MessageView/utils.ts diff --git a/src/ui/compoments/RawModeExitPrompt/index.tsx b/src/ui/components/RawModeExitPrompt/index.tsx similarity index 100% rename from src/ui/compoments/RawModeExitPrompt/index.tsx rename to src/ui/components/RawModeExitPrompt/index.tsx diff --git a/src/ui/compoments/RawModelDropdown/index.tsx b/src/ui/components/RawModelDropdown/index.tsx similarity index 100% rename from src/ui/compoments/RawModelDropdown/index.tsx rename to src/ui/components/RawModelDropdown/index.tsx diff --git a/src/ui/compoments/index.ts b/src/ui/components/index.ts similarity index 100% rename from src/ui/compoments/index.ts rename to src/ui/components/index.ts diff --git a/src/ui/index.ts b/src/ui/index.ts index f2e698c1..aa757f9a 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -9,8 +9,8 @@ export { } from "./App"; export { default as AppContainer } from "./AppContainer"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; -export { MessageView } from "./compoments"; -export { parseDiffPreview } from "./compoments/MessageView/utils"; +export { MessageView } from "./components"; +export { parseDiffPreview } from "./components/MessageView/utils"; export { PromptInput, IMAGE_ATTACHMENT_CLEAR_HINT, @@ -48,7 +48,7 @@ export { } from "./askUserQuestion"; export { readClipboardImage, type ClipboardImage } from "./clipboard"; export { buildLoadingText, type LoadingTextInput } from "./loadingText"; -export { renderMarkdown } from "./compoments/MessageView/markdown"; +export { renderMarkdown } from "./components/MessageView/markdown"; export { EMPTY_BUFFER, insertText, From bbf810d1a55bfa2c45cdf576ed186f7d0c606715 Mon Sep 17 00:00:00 2001 From: Seunghoon Shin Date: Wed, 20 May 2026 12:13:43 +0900 Subject: [PATCH 030/212] fix: resolve CJK composition bug on iOS terminals (backspace packet splitting) --- src/ui/prompt/useTerminalInput.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/ui/prompt/useTerminalInput.ts b/src/ui/prompt/useTerminalInput.ts index 8013ff60..8fe0d60b 100644 --- a/src/ui/prompt/useTerminalInput.ts +++ b/src/ui/prompt/useTerminalInput.ts @@ -193,6 +193,30 @@ export function useTerminalInput( return; } const handleData = (data: Buffer | string) => { + const raw = String(data); + + // Fix CJK composition bug on iOS terminals (Moshi, Blink, etc.). + // iOS keyboards send composed characters as a single packet like: + // "가\x7f나" (character + backspace + new character) + // Without splitting, parseTerminalInput treats the whole packet as + // one input and drops the composition backspaces, corrupting the text. + if (raw.includes("\x7f") && raw.length > 1) { + const parts = raw.split("\x7f"); + if (parts[0]) { + const { input, key } = parseTerminalInput(parts[0]); + handlerRef.current(input, key); + } + for (let i = 1; i < parts.length; i++) { + const bs = parseTerminalInput("\x7f"); + handlerRef.current(bs.input, bs.key); + if (parts[i]) { + const { input, key } = parseTerminalInput(parts[i]); + handlerRef.current(input, key); + } + } + return; + } + const { input, key } = parseTerminalInput(data); handlerRef.current(input, key); }; From 4605be4e3ccffa9a48d9203a32a1f5db5f0a0516 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 11:18:23 +0800 Subject: [PATCH 031/212] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=92=8C=E6=8A=80=E8=83=BD=E9=80=89=E6=8B=A9=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E8=8F=9C=E5=8D=95=E7=BB=84=E4=BB=B6=E5=B9=B6=E9=9B=86?= =?UTF-8?q?=E6=88=90=E5=88=B0PromptInput?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 ModelsDropdown 组件支持选择模型及思考模式 - 创建 SkillsDropdown 组件支持选择和切换技能 - 在 ui/components/index.ts 中导出新增组件 - 在 ui/index.ts 中导出 ModelsDropdown 相关辅助方法 - 在 PromptInput 组件中替换旧模型选择逻辑,改用新增下拉组件 - 优化 PromptInput 的快捷键处理,实现技能和模型菜单切换 - 移除 PromptInput 内部的模型选择状态及逻辑,简化代码结构 - 保持现有功能一致,增加用户界面交互的灵活性与可用性 --- src/ui/PromptInput.tsx | 231 +++------------------ src/ui/components/ModelsDropdown/index.tsx | 165 +++++++++++++++ src/ui/components/SkillsDropdown/index.tsx | 74 +++++++ src/ui/components/index.ts | 2 + src/ui/index.ts | 10 +- 5 files changed, 275 insertions(+), 207 deletions(-) create mode 100644 src/ui/components/ModelsDropdown/index.tsx create mode 100644 src/ui/components/SkillsDropdown/index.tsx diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index b9b1f8e5..a79fe304 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -49,9 +49,9 @@ import { useTerminalInput } from "./prompt"; import type { InputKey } from "./prompt"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, useTerminalFocusReporting } from "./prompt"; import SlashCommandMenu from "./SlashCommandMenu"; -import type { ModelConfigSelection, ReasoningEffort } from "../settings"; +import type { ModelConfigSelection } from "../settings"; import DropdownMenu from "./DropdownMenu"; -import { RawModelDropdown } from "./components"; +import { ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; export type PromptSubmission = { text: string; @@ -79,21 +79,6 @@ type Props = { }; const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; -export const MODEL_COMMAND_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"] as const; - -type ThinkingModeOption = { - label: string; - thinkingEnabled: boolean; - reasoningEffort?: ReasoningEffort; -}; - -export const MODEL_COMMAND_THINKING_OPTIONS: ThinkingModeOption[] = [ - { label: "Thinking mode [max]", thinkingEnabled: true, reasoningEffort: "max" }, - { label: "Thinking mode [high]", thinkingEnabled: true, reasoningEffort: "high" }, - { label: "No thinking", thinkingEnabled: false }, -]; - -type ModelDropdownStep = "model" | "thinking"; const PromptPrefixLine = React.memo(function PromptPrefixLine({ busy }: { busy: boolean }): React.ReactElement { const [spinnerIndex, setSpinnerIndex] = useState(0); @@ -140,10 +125,7 @@ export const PromptInput = React.memo(function PromptInput({ const [menuIndex, setMenuIndex] = useState(0); const [showSkillsDropdown, setShowSkillsDropdown] = useState(false); const [openRawModelDropdown, setOpenRawModelDropdown] = useState(false); - const [skillsDropdownIndex, setSkillsDropdownIndex] = useState(0); - const [modelDropdownStep, setModelDropdownStep] = useState(null); - const [modelDropdownIndex, setModelDropdownIndex] = useState(0); - const [pendingModel, setPendingModel] = useState(null); + const [showModelDropdown, setShowModelDropdown] = useState(false); const [fileMentionItems, setFileMentionItems] = useState(() => scanFileMentionItems(projectRoot)); const [fileMentionIndex, setFileMentionIndex] = useState(0); const [dismissedFileMentionKey, setDismissedFileMentionKey] = useState(null); @@ -164,19 +146,19 @@ export const PromptInput = React.memo(function PromptInput({ ); const showFileMentionMenu = !showSkillsDropdown && - !modelDropdownStep && + !showModelDropdown && fileMentionToken !== null && fileMentionKey !== dismissedFileMentionKey; const slashItems = React.useMemo(() => buildSlashCommands(skills), [skills]); const slashToken = getCurrentSlashToken(buffer); const slashMenu = React.useMemo( () => - showSkillsDropdown || modelDropdownStep || showFileMentionMenu + showSkillsDropdown || showModelDropdown || showFileMentionMenu ? [] : slashToken ? filterSlashCommands(slashItems, slashToken) : [], - [showSkillsDropdown, modelDropdownStep, showFileMentionMenu, slashToken, slashItems] + [showSkillsDropdown, showModelDropdown, showFileMentionMenu, slashToken, slashItems] ); const showMenu = slashMenu.length > 0; const promptHistoryKey = React.useMemo(() => promptHistory.join("\0"), [promptHistory]); @@ -241,23 +223,6 @@ export const PromptInput = React.memo(function PromptInput({ } }, [fileMentionMatches.length, fileMentionIndex, showFileMentionMenu]); - useEffect(() => { - if (skillsDropdownIndex >= skills.length) { - setSkillsDropdownIndex(Math.max(0, skills.length - 1)); - } - }, [skills.length, skillsDropdownIndex]); - - useEffect(() => { - if (!modelDropdownStep) { - return; - } - const optionCount = - modelDropdownStep === "model" ? MODEL_COMMAND_MODELS.length : MODEL_COMMAND_THINKING_OPTIONS.length; - if (modelDropdownIndex >= optionCount) { - setModelDropdownIndex(Math.max(0, optionCount - 1)); - } - }, [modelDropdownIndex, modelDropdownStep]); - useEffect(() => { if (!statusMessage) { return; @@ -287,14 +252,6 @@ export const PromptInput = React.memo(function PromptInput({ } if (key.escape) { - if (modelDropdownStep) { - closeModelDropdown(); - return; - } - if (showSkillsDropdown) { - setShowSkillsDropdown(false); - return; - } if (showFileMentionMenu && fileMentionKey) { setDismissedFileMentionKey(fileMentionKey); return; @@ -348,7 +305,7 @@ export const PromptInput = React.memo(function PromptInput({ setPendingExit(false); } - if (openRawModelDropdown) { + if (openRawModelDropdown || showSkillsDropdown || showModelDropdown) { return; } @@ -356,53 +313,6 @@ export const PromptInput = React.memo(function PromptInput({ exitHistoryBrowsing(); } - if (showSkillsDropdown) { - if (skills.length === 0) { - setShowSkillsDropdown(false); - } else { - if (key.upArrow) { - setSkillsDropdownIndex((idx) => (idx - 1 + skills.length) % skills.length); - return; - } - if (key.downArrow) { - setSkillsDropdownIndex((idx) => (idx + 1) % skills.length); - return; - } - if ((input === " " && !key.ctrl && !key.meta) || (key.return && !key.shift && !key.meta)) { - const skill = skills[skillsDropdownIndex]; - if (skill) { - toggleSelectedSkill(skill); - } - return; - } - if (key.tab) { - setShowSkillsDropdown(false); - return; - } - } - } - - if (modelDropdownStep) { - const optionCount = - modelDropdownStep === "model" ? MODEL_COMMAND_MODELS.length : MODEL_COMMAND_THINKING_OPTIONS.length; - if (key.upArrow) { - setModelDropdownIndex((idx) => (idx - 1 + optionCount) % optionCount); - return; - } - if (key.downArrow) { - setModelDropdownIndex((idx) => (idx + 1) % optionCount); - return; - } - if ((input === " " && !key.ctrl && !key.meta) || (key.return && !key.shift && !key.meta)) { - selectModelDropdownItem(); - return; - } - if (key.tab) { - closeModelDropdown(); - return; - } - } - if (key.ctrl && (input === "v" || input === "V")) { setStatusMessage("Reading clipboard..."); readClipboardImageAsync() @@ -722,7 +632,8 @@ export const PromptInput = React.memo(function PromptInput({ } if (item.kind === "model") { clearSlashToken(); - openModelDropdown(); + setShowSkillsDropdown(false); + setShowModelDropdown(true); return; } if (item.kind === "raw") { @@ -828,63 +739,9 @@ export const PromptInput = React.memo(function PromptInput({ clearUndoRedoStacks(); } - function openModelDropdown(): void { - const currentModelIndex = MODEL_COMMAND_MODELS.findIndex((model) => model === modelConfig.model); - setPendingModel(null); - setModelDropdownStep("model"); - setModelDropdownIndex(currentModelIndex >= 0 ? currentModelIndex : 0); - setShowSkillsDropdown(false); - } - - function closeModelDropdown(): void { - setModelDropdownStep(null); - setPendingModel(null); - } - - function selectModelDropdownItem(): void { - if (modelDropdownStep === "model") { - const model = MODEL_COMMAND_MODELS[modelDropdownIndex] ?? modelConfig.model; - setPendingModel(model); - setModelDropdownStep("thinking"); - setModelDropdownIndex(getThinkingOptionIndex(modelConfig)); - return; - } - - const option = MODEL_COMMAND_THINKING_OPTIONS[modelDropdownIndex] ?? MODEL_COMMAND_THINKING_OPTIONS[0]; - const selection: ModelConfigSelection = { - model: pendingModel ?? modelConfig.model, - thinkingEnabled: option.thinkingEnabled, - reasoningEffort: option.reasoningEffort ?? modelConfig.reasoningEffort, - }; - closeModelDropdown(); - Promise.resolve(onModelConfigChange(selection)) - .then((message) => { - if (message) { - setStatusMessage(message); - } - }) - .catch((error) => { - const message = error instanceof Error ? error.message : String(error); - setStatusMessage(`Failed to update model settings: ${message}`); - }); - } - - const modelDropdownItems = - modelDropdownStep === "model" - ? MODEL_COMMAND_MODELS.map((model) => ({ - label: model, - selected: model === (pendingModel ?? modelConfig.model), - description: model === modelConfig.model ? "current model" : "", - })) - : MODEL_COMMAND_THINKING_OPTIONS.map((option) => ({ - label: option.label, - selected: getThinkingOptionIndex(modelConfig) === MODEL_COMMAND_THINKING_OPTIONS.indexOf(option), - description: option.thinkingEnabled ? `reasoningEffort: ${option.reasoningEffort}` : "thinking disabled", - })); - const showFooterText = useMemo( - () => showMenu || showSkillsDropdown || openRawModelDropdown || modelDropdownStep !== null || showFileMentionMenu, - [showMenu, showSkillsDropdown, modelDropdownStep, openRawModelDropdown, showFileMentionMenu] + () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, + [showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu] ); const matchedCommand = slashToken ? findExactSlashCommand(slashItems, slashToken) : null; @@ -925,44 +782,22 @@ export const PromptInput = React.memo(function PromptInput({ onSelect={(mode) => onRawModeChange?.(mode)} screenWidth={screenWidth} /> - {showSkillsDropdown ? ( - ({ - key: skill.path || skill.name, - label: skill.name, - description: skill.path, - selected: isSkillSelected(selectedSkills, skill), - statusIndicator: skill.isLoaded ? { symbol: "✓", color: "green" } : undefined, - }))} - activeIndex={skillsDropdownIndex} - activeColor="#229ac3" - maxVisible={6} - /> - ) : null} - {modelDropdownStep ? ( - ({ - key: item.label, - label: item.label, - description: item.description, - selected: item.selected, - }))} - activeIndex={modelDropdownIndex} - activeColor="#229ac3" - maxVisible={6} - /> - ) : null} + + setShowModelDropdown(false)} + onModelConfigChange={onModelConfigChange} + onStatusMessage={setStatusMessage} + /> {showFileMentionMenu ? ( -): number { - const index = MODEL_COMMAND_THINKING_OPTIONS.findIndex((option) => { - if (!config.thinkingEnabled) { - return !option.thinkingEnabled; - } - return option.thinkingEnabled && option.reasoningEffort === config.reasoningEffort; - }); - return index >= 0 ? index : 0; -} - export function removeCurrentSlashToken(state: PromptBufferState): PromptBufferState { let start = state.cursor; while (start > 0 && !/\s/.test(state.text[start - 1] ?? "")) { diff --git a/src/ui/components/ModelsDropdown/index.tsx b/src/ui/components/ModelsDropdown/index.tsx new file mode 100644 index 00000000..bdd68ab4 --- /dev/null +++ b/src/ui/components/ModelsDropdown/index.tsx @@ -0,0 +1,165 @@ +import React, { useEffect, useState } from "react"; +import { useInput } from "ink"; +import DropdownMenu from "../../DropdownMenu"; +import type { ModelConfigSelection, ReasoningEffort } from "../../../settings"; + +type ModelStep = "model" | "thinking"; + +type ThinkingModeOption = { + label: string; + thinkingEnabled: boolean; + reasoningEffort?: ReasoningEffort; +}; + +export const MODEL_COMMAND_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"] as const; + +export const MODEL_COMMAND_THINKING_OPTIONS: ThinkingModeOption[] = [ + { label: "Thinking mode [max]", thinkingEnabled: true, reasoningEffort: "max" }, + { label: "Thinking mode [high]", thinkingEnabled: true, reasoningEffort: "high" }, + { label: "No thinking", thinkingEnabled: false }, +]; + +function getThinkingOptionIndex(config: Pick): number { + const index = MODEL_COMMAND_THINKING_OPTIONS.findIndex((option) => { + if (!config.thinkingEnabled) { + return !option.thinkingEnabled; + } + return option.thinkingEnabled && option.reasoningEffort === config.reasoningEffort; + }); + return index >= 0 ? index : 0; +} + +type Props = { + open: boolean; + modelConfig: ModelConfigSelection; + width: number; + onClose: () => void; + onModelConfigChange: (selection: ModelConfigSelection) => string | Promise; + onStatusMessage?: (message: string | null) => void; +}; + +const ModelsDropdown: React.FC = ({ + open, + modelConfig, + width, + onClose, + onModelConfigChange, + onStatusMessage, +}) => { + const [step, setStep] = useState(null); + const [activeIndex, setActiveIndex] = useState(0); + const [pendingModel, setPendingModel] = useState(null); + + // Initialize state when opened + useEffect(() => { + if (open) { + const currentIndex = MODEL_COMMAND_MODELS.findIndex((m) => m === modelConfig.model); + setPendingModel(null); + setStep("model"); + setActiveIndex(currentIndex >= 0 ? currentIndex : 0); + } else { + setStep(null); + } + }, [open, modelConfig.model]); + + // Validate activeIndex bounds + useEffect(() => { + if (!step) { + return; + } + const optionCount = step === "model" ? MODEL_COMMAND_MODELS.length : MODEL_COMMAND_THINKING_OPTIONS.length; + if (activeIndex >= optionCount) { + setActiveIndex(Math.max(0, optionCount - 1)); + } + }, [activeIndex, step]); + + function selectItem(): void { + if (step === "model") { + const model = MODEL_COMMAND_MODELS[activeIndex] ?? modelConfig.model; + setPendingModel(model); + setStep("thinking"); + setActiveIndex(getThinkingOptionIndex(modelConfig)); + return; + } + + const option = MODEL_COMMAND_THINKING_OPTIONS[activeIndex] ?? MODEL_COMMAND_THINKING_OPTIONS[0]!; + const selection: ModelConfigSelection = { + model: pendingModel ?? modelConfig.model, + thinkingEnabled: option.thinkingEnabled, + reasoningEffort: option.reasoningEffort ?? modelConfig.reasoningEffort, + }; + onClose(); + Promise.resolve(onModelConfigChange(selection)) + .then((message) => { + if (message) { + onStatusMessage?.(message); + } + }) + .catch((error) => { + const msg = error instanceof Error ? error.message : String(error); + onStatusMessage?.(`Failed to update model settings: ${msg}`); + }); + } + + useInput( + (input, key) => { + if (!step) { + return; + } + + const optionCount = step === "model" ? MODEL_COMMAND_MODELS.length : MODEL_COMMAND_THINKING_OPTIONS.length; + + if (key.upArrow) { + setActiveIndex((idx) => (idx - 1 + optionCount) % optionCount); + return; + } + if (key.downArrow) { + setActiveIndex((idx) => (idx + 1) % optionCount); + return; + } + if ((input === " " && !key.ctrl && !key.meta) || (key.return && !key.shift && !key.meta)) { + selectItem(); + return; + } + if (key.tab || key.escape) { + onClose(); + return; + } + }, + { isActive: open } + ); + + if (!open || !step) { + return null; + } + + const items = + step === "model" + ? MODEL_COMMAND_MODELS.map((model) => ({ + key: model, + label: model, + description: model === modelConfig.model ? "current model" : "", + selected: model === (pendingModel ?? modelConfig.model), + })) + : MODEL_COMMAND_THINKING_OPTIONS.map((option, i) => ({ + key: option.label, + label: option.label, + description: option.thinkingEnabled ? `reasoningEffort: ${option.reasoningEffort}` : "thinking disabled", + selected: getThinkingOptionIndex(modelConfig) === i, + })); + + return ( + + ); +}; + +export { getThinkingOptionIndex }; +export default ModelsDropdown; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx new file mode 100644 index 00000000..545e2abd --- /dev/null +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -0,0 +1,74 @@ +import DropdownMenu from "../../DropdownMenu"; +import React, { useEffect, useState } from "react"; +import { isSkillSelected } from "../../PromptInput"; +import type { SkillInfo } from "../../../session"; +import { useInput } from "ink"; + +const SkillsDropdown: React.FC<{ + open: boolean; + onClose?: (value: boolean) => void; + width: number; + skills: SkillInfo[]; + selectedSkills: SkillInfo[]; + onSelect?: (skill: SkillInfo) => void; +}> = ({ open, width, skills, selectedSkills, onSelect, onClose }) => { + const [skillsDropdownIndex, setSkillsDropdownIndex] = useState(0); + useInput( + (input, key) => { + if (key.upArrow) { + setSkillsDropdownIndex((idx) => (idx - 1 + skills.length) % skills.length); + return; + } + if (key.downArrow) { + setSkillsDropdownIndex((idx) => (idx + 1) % skills.length); + return; + } + if ((input === " " && !key.ctrl && !key.meta) || (key.return && !key.shift && !key.meta)) { + const skill = skills[skillsDropdownIndex]; + if (skill) { + onSelect?.(skill); + } + return; + } + if (key.tab) { + onClose?.(false); + return; + } + if (key.escape) { + onClose?.(false); + } + }, + { isActive: open } + ); + + useEffect(() => { + if (skillsDropdownIndex >= skills.length) { + setSkillsDropdownIndex(Math.max(0, skills.length - 1)); + } + }, [skills.length, skillsDropdownIndex]); + + if (!open) { + return null; + } + + return ( + ({ + key: skill.path || skill.name, + label: skill.name, + description: skill.path, + selected: isSkillSelected(selectedSkills, skill), + statusIndicator: skill.isLoaded ? { symbol: "✓", color: "green" } : undefined, + }))} + activeIndex={skillsDropdownIndex} + activeColor="#229ac3" + maxVisible={6} + /> + ); +}; + +export default SkillsDropdown; diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index 942d3ed1..1d929f36 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1,3 +1,5 @@ export { default as RawModelDropdown } from "./RawModelDropdown"; export { MessageView } from "./MessageView"; export { RawModeExitPrompt } from "./RawModeExitPrompt"; +export { default as SkillsDropdown } from "./SkillsDropdown"; +export { default as ModelsDropdown } from "./ModelsDropdown"; diff --git a/src/ui/index.ts b/src/ui/index.ts index aa757f9a..efb4edd5 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -1,3 +1,9 @@ +import { + getThinkingOptionIndex, + MODEL_COMMAND_MODELS, + MODEL_COMMAND_THINKING_OPTIONS, +} from "./components/ModelsDropdown"; + export { readSettings, readProjectSettings, @@ -24,14 +30,12 @@ export { getPromptReturnKeyAction, renderBufferWithCursor, buildInitPromptSubmission, - getThinkingOptionIndex, - MODEL_COMMAND_MODELS, - MODEL_COMMAND_THINKING_OPTIONS, useTerminalInput, parseTerminalInput, type PromptSubmission, type InputKey, } from "./PromptInput"; +export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./prompt/cursor"; export { SessionList, formatSessionTitle } from "./SessionList"; export { ThemedGradient } from "./ThemedGradient"; From 02865704effba4ee50201fd6389f48744293e108 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 20 May 2026 11:35:20 +0800 Subject: [PATCH 032/212] feat: add undo functionality and enhance session management --- src/session.ts | 302 ++++++++++++++++++++++++++++++ src/tests/promptInputKeys.test.ts | 28 ++- src/tests/session.test.ts | 296 +++++++++++++++++++++++++++++ src/tests/slashCommands.test.ts | 20 +- src/tools/edit-handler.ts | 2 + src/tools/executor.ts | 6 + src/tools/write-handler.ts | 2 + src/ui/App.tsx | 106 ++++++++++- src/ui/PromptInput.tsx | 36 +++- src/ui/UndoSelector.tsx | 195 +++++++++++++++++++ src/ui/index.ts | 2 + src/ui/slashCommands.ts | 7 + 12 files changed, 997 insertions(+), 5 deletions(-) create mode 100644 src/ui/UndoSelector.tsx diff --git a/src/session.ts b/src/session.ts index 3f79481c..6b2ceee0 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; +import * as childProcess from "child_process"; import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; @@ -34,6 +35,8 @@ const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; +const FILE_HISTORY_AUTHOR_NAME = "DeepCode Checkpoint"; +const FILE_HISTORY_AUTHOR_EMAIL = "deepcode-checkpoint@localhost"; type ChatCompletionDebugOptions = { enabled?: boolean; @@ -202,6 +205,13 @@ export type SessionMessage = { updateTime: string; meta?: MessageMeta; html?: string; + checkpointHash?: string; +}; + +export type UndoTarget = { + message: SessionMessage; + index: number; + canRestoreCode: boolean; }; export type UserPromptContent = { @@ -902,6 +912,7 @@ The candidate skills are as follows:\n\n`; userPrompt.skills = await this.normalizeSkills(userPrompt.skills); this.throwIfAborted(signal); const sessionId = crypto.randomUUID(); + this.ensureFileHistorySession(sessionId); const now = new Date().toISOString(); const index = this.loadSessionsIndex(); const entry: SessionEntry = { @@ -1022,6 +1033,7 @@ ${skillMd} userPrompt.skills = await this.normalizeSkills(userPrompt.skills, sessionId); this.throwIfAborted(signal); + this.ensureFileHistorySession(sessionId); const userMessage = this.buildUserMessage(sessionId, userPrompt); this.appendSessionMessage(sessionId, userMessage); @@ -1480,6 +1492,61 @@ ${skillMd} return messages; } + listUndoTargets(sessionId: string): UndoTarget[] { + return this.listSessionMessages(sessionId) + .map((message, index) => ({ message, index })) + .filter(({ message }) => this.isUndoTargetMessage(message)) + .map(({ message, index }) => ({ + message, + index, + canRestoreCode: Boolean( + message.checkpointHash && this.canRestoreCheckpointHash(sessionId, message.checkpointHash) + ), + })); + } + + restoreSessionConversation(sessionId: string, messageId: string): SessionMessage[] { + const messages = this.listSessionMessages(sessionId); + const targetIndex = messages.findIndex((message) => message.id === messageId); + if (targetIndex === -1) { + throw new Error("Selected message was not found in this session."); + } + + const keptMessages = messages.slice(0, targetIndex); + this.saveSessionMessages(sessionId, keptMessages); + const now = new Date().toISOString(); + const latestAssistant = [...keptMessages].reverse().find((message) => message.role === "assistant"); + const latestAssistantParams = latestAssistant?.messageParams as + | { tool_calls?: unknown[]; reasoning_content?: string } + | null + | undefined; + + this.updateSessionEntry(sessionId, (entry) => ({ + ...entry, + assistantReply: latestAssistant?.content ?? null, + assistantThinking: + typeof latestAssistantParams?.reasoning_content === "string" ? latestAssistantParams.reasoning_content : null, + assistantRefusal: null, + toolCalls: null, + status: "completed", + failReason: null, + processes: null, + updateTime: now, + })); + return keptMessages; + } + + restoreSessionCode(sessionId: string, messageId: string): void { + const message = this.listSessionMessages(sessionId).find((item) => item.id === messageId); + if (!message) { + throw new Error("Selected message was not found in this session."); + } + if (!message.checkpointHash) { + throw new Error("Selected message has no code checkpoint."); + } + this.restoreCheckpointHash(sessionId, message.checkpointHash); + } + private normalizeSessionMessage(message: SessionMessage): SessionMessage { if (message.role !== "tool") { return message; @@ -1518,6 +1585,238 @@ ${skillMd} return { projectCode, projectDir, sessionsIndexPath }; } + private getFileHistoryGitDir(): string { + const { projectDir } = this.getProjectStorage(); + return path.join(projectDir, "file-history", ".git"); + } + + private getSessionBranchRef(sessionId: string): string | null { + if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) { + return null; + } + return `refs/heads/${sessionId}`; + } + + private ensureFileHistorySession(sessionId: string): string | undefined { + const branchRef = this.getSessionBranchRef(sessionId); + if (!branchRef) { + return undefined; + } + + try { + const gitDir = this.getFileHistoryGitDir(); + if (!fs.existsSync(gitDir)) { + fs.mkdirSync(path.dirname(gitDir), { recursive: true }); + this.runFileHistoryGit(["init"], { includeWorkTree: true }); + } + + const current = this.getCurrentCheckpointHash(sessionId); + if (current) { + return current; + } + + const emptyTree = this.runFileHistoryGit(["mktree"], { includeWorkTree: false, input: "" }).trim(); + const commitHash = this.createFileHistoryCommit(emptyTree, null, "Initial checkpoint"); + this.runFileHistoryGit(["update-ref", branchRef, commitHash], { includeWorkTree: false }); + return commitHash; + } catch { + return undefined; + } + } + + private getCurrentCheckpointHash(sessionId: string): string | undefined { + const gitDir = this.getFileHistoryGitDir(); + const branchRef = this.getSessionBranchRef(sessionId); + if (!branchRef || !fs.existsSync(gitDir)) { + return undefined; + } + + try { + const hash = this.runFileHistoryGit(["rev-parse", "--verify", `${branchRef}^{commit}`], { + includeWorkTree: false, + }).trim(); + return this.isCommitHash(hash) ? hash : undefined; + } catch { + return undefined; + } + } + + private prepareFileMutationCheckpoint(sessionId: string, filePath: string): void { + const previousHash = this.ensureFileHistorySession(sessionId); + if (!previousHash) { + return; + } + this.updateLatestUserCheckpointHash(sessionId, undefined, previousHash); + const nextHash = this.recordFileHistoryCheckpoint(sessionId, [filePath], "Pre-mutation checkpoint"); + if (nextHash && nextHash !== previousHash) { + this.updateLatestUserCheckpointHash(sessionId, previousHash, nextHash); + } + } + + private recordFileMutationCheckpoint(sessionId: string, filePath: string): void { + this.ensureFileHistorySession(sessionId); + this.recordFileHistoryCheckpoint(sessionId, [filePath], "File mutation checkpoint"); + } + + private recordFileHistoryCheckpoint(sessionId: string, filePaths: string[], message: string): string | undefined { + const branchRef = this.getSessionBranchRef(sessionId); + if (!branchRef) { + return undefined; + } + + const relativePaths = filePaths + .map((filePath) => this.toProjectRelativeGitPath(filePath)) + .filter((filePath): filePath is string => Boolean(filePath)); + if (relativePaths.length === 0) { + return this.getCurrentCheckpointHash(sessionId); + } + + try { + const parentHash = this.ensureFileHistorySession(sessionId); + if (!parentHash) { + return undefined; + } + this.runFileHistoryGit(["read-tree", "--reset", branchRef], { includeWorkTree: true }); + this.runFileHistoryGit(["add", "-f", "-A", "--", ...relativePaths], { includeWorkTree: true }); + const treeHash = this.runFileHistoryGit(["write-tree"], { includeWorkTree: false }).trim(); + const parentTreeHash = this.runFileHistoryGit(["rev-parse", `${parentHash}^{tree}`], { + includeWorkTree: false, + }).trim(); + if (treeHash === parentTreeHash) { + return parentHash; + } + + const commitHash = this.createFileHistoryCommit(treeHash, parentHash, message); + this.runFileHistoryGit(["update-ref", branchRef, commitHash, parentHash], { includeWorkTree: false }); + return commitHash; + } catch { + return undefined; + } + } + + private updateLatestUserCheckpointHash(sessionId: string, previousHash: string | undefined, nextHash: string): void { + const messages = this.listSessionMessages(sessionId); + for (let index = messages.length - 1; index >= 0; index -= 1) { + const message = messages[index]; + if (!message || !this.isUndoTargetMessage(message)) { + continue; + } + if (message.checkpointHash && message.checkpointHash !== previousHash) { + return; + } + messages[index] = { + ...message, + checkpointHash: nextHash, + updateTime: new Date().toISOString(), + }; + this.saveSessionMessages(sessionId, messages); + return; + } + } + + private createFileHistoryCommit(treeHash: string, parentHash: string | null, message: string): string { + const args = ["commit-tree", treeHash]; + if (parentHash) { + args.push("-p", parentHash); + } + args.push("-m", message); + return this.runFileHistoryGit(args, { + includeWorkTree: false, + env: this.getFileHistoryGitEnv(), + }).trim(); + } + + private getFileHistoryGitEnv(): NodeJS.ProcessEnv { + return { + ...process.env, + GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || FILE_HISTORY_AUTHOR_NAME, + GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || FILE_HISTORY_AUTHOR_EMAIL, + GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || FILE_HISTORY_AUTHOR_NAME, + GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || FILE_HISTORY_AUTHOR_EMAIL, + }; + } + + private toProjectRelativeGitPath(filePath: string): string | null { + const absolutePath = path.resolve(filePath); + const relativePath = path.relative(this.projectRoot, absolutePath); + if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + return null; + } + return relativePath.split(path.sep).join("/"); + } + + private canRestoreCheckpointHash(sessionId: string, checkpointHash: string): boolean { + if (!this.isCommitHash(checkpointHash)) { + return false; + } + if (!this.getSessionBranchRef(sessionId)) { + return false; + } + const gitDir = this.getFileHistoryGitDir(); + if (!fs.existsSync(gitDir)) { + return false; + } + + try { + this.runFileHistoryGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false }); + return true; + } catch { + return false; + } + } + + private restoreCheckpointHash(sessionId: string, checkpointHash: string): void { + if (!this.isCommitHash(checkpointHash)) { + throw new Error("Invalid checkpoint hash."); + } + const gitDir = this.getFileHistoryGitDir(); + const branchRef = this.getSessionBranchRef(sessionId); + if (!branchRef || !fs.existsSync(gitDir)) { + throw new Error("File history Git repository was not found for this project."); + } + this.runFileHistoryGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false }); + + try { + this.runFileHistoryGit(["read-tree", "--reset", branchRef], { includeWorkTree: true }); + } catch { + // If the session branch is missing, fall back to the target tree only. + // The target checkpoint has already been validated above. + } + this.runFileHistoryGit(["read-tree", "--reset", "-u", checkpointHash], { includeWorkTree: true }); + this.runFileHistoryGit(["update-ref", branchRef, checkpointHash], { includeWorkTree: false }); + } + + private runFileHistoryGit( + args: string[], + options: { includeWorkTree: boolean; input?: string; env?: NodeJS.ProcessEnv } + ): string { + const gitDir = this.getFileHistoryGitDir(); + const gitArgs = [`--git-dir=${gitDir}`]; + if (options.includeWorkTree) { + gitArgs.push(`--work-tree=${this.projectRoot}`); + } + gitArgs.push(...args); + const result = childProcess.spawnSync("git", gitArgs, { + encoding: "utf8", + input: options.input, + env: options.env, + stdio: ["pipe", "pipe", "pipe"], + }); + if (result.status !== 0) { + const detail = (result.stderr || result.stdout || "").trim(); + throw new Error(detail || `git ${args.join(" ")} failed`); + } + return result.stdout ?? ""; + } + + private isCommitHash(value: string): boolean { + return /^[0-9a-f]{40}$/i.test(value); + } + + private isUndoTargetMessage(message: SessionMessage): boolean { + return message.role === "user" && message.visible && !message.compacted; + } + private ensureProjectDir(): string { const { projectDir } = this.getProjectStorage(); fs.mkdirSync(projectDir, { recursive: true }); @@ -1628,6 +1927,7 @@ ${skillMd} visible: true, createTime: now, updateTime: now, + checkpointHash: this.getCurrentCheckpointHash(sessionId), }; } @@ -1795,6 +2095,8 @@ ${skillMd} onProcessExit: (pid) => this.removeSessionProcess(sessionId, pid), onProcessStdout: (pid, chunk) => this.onProcessStdout?.(Number(pid), chunk), onProcessTimeoutControl: (pid, control) => this.setSessionProcessTimeoutControl(sessionId, pid, control), + onBeforeFileMutation: (filePath) => this.prepareFileMutationCheckpoint(sessionId, filePath), + onAfterFileMutation: (filePath) => this.recordFileMutationCheckpoint(sessionId, filePath), shouldStop: () => this.isInterrupted(sessionId), }); if (this.isInterrupted(sessionId)) { diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts index 69d20758..54213a12 100644 --- a/src/tests/promptInputKeys.test.ts +++ b/src/tests/promptInputKeys.test.ts @@ -19,10 +19,11 @@ import { toggleSkillSelection, renderBufferWithCursor, buildInitPromptSubmission, + buildPromptDraftFromSessionMessage, disableTerminalExtendedKeys, enableTerminalExtendedKeys, } from "../ui"; -import type { SkillInfo } from "../session"; +import type { SessionMessage, SkillInfo } from "../session"; test("parseTerminalInput treats DEL bytes as backspace", () => { const { input, key } = parseTerminalInput("\u007F"); @@ -112,6 +113,31 @@ test("terminal extended key helpers request and restore modifyOtherKeys mode", ( assert.equal(disableTerminalExtendedKeys(), "\u001B[>4;0m"); }); +test("buildPromptDraftFromSessionMessage restores text and image urls", () => { + const message: SessionMessage = { + id: "user-with-images", + sessionId: "session-1", + role: "user", + content: "revise this prompt", + contentParams: [ + { type: "image_url", image_url: { url: "data:image/png;base64,abc" } }, + { type: "text", text: "ignored" }, + { type: "image_url", image_url: { url: "data:image/jpeg;base64,def" } }, + ], + messageParams: null, + compacted: false, + visible: true, + createTime: "2026-01-01T00:00:00.000Z", + updateTime: "2026-01-01T00:00:00.000Z", + }; + + assert.deepEqual(buildPromptDraftFromSessionMessage(message, 7), { + nonce: 7, + text: "revise this prompt", + imageUrls: ["data:image/png;base64,abc", "data:image/jpeg;base64,def"], + }); +}); + test("parseTerminalInput recognizes terminal focus events", () => { const focusIn = parseTerminalInput("\u001B[I"); const focusOut = parseTerminalInput("\u001B[O"); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index bfe5bad2..c02c0fa3 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1,5 +1,6 @@ import { afterEach, test } from "node:test"; import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; @@ -887,6 +888,219 @@ test("replySession continues without appending /continue as a user message", asy assert.equal(fetchCalls.length, 0); }); +test("replySession records the current file-history branch head as checkpointHash", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-checkpoint-hash-workspace-"); + const home = createTempDir("deepcode-checkpoint-hash-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-checkpoint-hash"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const checkpointHash = createFileHistoryCommit(home, workspace, sessionId, { "note.txt": "checkpoint\n" }); + + await manager.replySession(sessionId, { text: "second prompt" }); + + const userMessages = manager.listSessionMessages(sessionId).filter((message) => message.role === "user"); + assert.equal(userMessages[userMessages.length - 1]?.checkpointHash, checkpointHash); +}); + +test("createSession initializes file-history repo and session branch", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-file-history-init-workspace-"); + const home = createTempDir("deepcode-file-history-init-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-file-history-init"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const userMessage = manager.listSessionMessages(sessionId).find((message) => message.role === "user"); + const gitDir = path.join( + home, + ".deepcode", + "projects", + workspace.replace(/[\\/]/g, "-").replace(/:/g, ""), + "file-history", + ".git" + ); + + assert.ok(fs.existsSync(gitDir)); + assert.ok(userMessage?.checkpointHash); + assert.equal( + runFileHistoryGit(gitDir, workspace, ["rev-parse", "--verify", `refs/heads/${sessionId}^{commit}`]).trim(), + userMessage.checkpointHash + ); +}); + +test("Write tool advances file-history while preserving the user prompt checkpoint", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-write-checkpoint-workspace-"); + const home = createTempDir("deepcode-write-checkpoint-home-"); + setHomeDir(home); + + const filePath = path.join(workspace, "index.html"); + const manager = createMockedClientSessionManager(workspace, [ + { + choices: [ + { + message: { + content: "", + tool_calls: [ + { + id: "call-write-index", + type: "function", + function: { + name: "write", + arguments: JSON.stringify({ file_path: filePath, content: "

Hello

\n" }), + }, + }, + ], + }, + }, + ], + }, + createChatResponse("done", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }), + ]); + + const sessionId = await manager.createSession({ text: "create an index page" }); + const userMessage = manager.listSessionMessages(sessionId).find((message) => message.role === "user"); + assert.ok(userMessage?.checkpointHash); + assert.equal(fs.existsSync(filePath), true); + + manager.restoreSessionCode(sessionId, userMessage.id); + + assert.equal(fs.existsSync(filePath), false); +}); + +test("missing git executable does not block sessions or Write tool calls", async () => { + const workspace = createTempDir("deepcode-no-git-write-workspace-"); + const home = createTempDir("deepcode-no-git-write-home-"); + setHomeDir(home); + + const originalPath = process.env.PATH; + process.env.PATH = ""; + try { + const filePath = path.join(workspace, "index.html"); + const manager = createMockedClientSessionManager(workspace, [ + { + choices: [ + { + message: { + content: "", + tool_calls: [ + { + id: "call-write-no-git", + type: "function", + function: { + name: "write", + arguments: JSON.stringify({ file_path: filePath, content: "

No Git

\n" }), + }, + }, + ], + }, + }, + ], + }, + createChatResponse("done", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }), + ]); + + const sessionId = await manager.createSession({ text: "create an index page" }); + const userMessage = manager.listSessionMessages(sessionId).find((message) => message.role === "user"); + + assert.equal(fs.readFileSync(filePath, "utf8"), "

No Git

\n"); + assert.equal(userMessage?.checkpointHash, undefined); + assert.equal(manager.getSession(sessionId)?.status, "completed"); + } finally { + if (originalPath === undefined) { + delete process.env.PATH; + } else { + process.env.PATH = originalPath; + } + } +}); + +test("restoreSessionConversation truncates messages before the selected user prompt", async () => { + const workspace = createTempDir("deepcode-undo-conversation-workspace-"); + const home = createTempDir("deepcode-undo-conversation-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-undo-conversation"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const firstAssistant = (manager as any).buildAssistantMessage( + sessionId, + "first answer", + null, + null + ) as SessionMessage; + (manager as any).appendSessionMessage(sessionId, firstAssistant); + await manager.replySession(sessionId, { text: "second prompt" }); + const secondUserMessage = manager + .listSessionMessages(sessionId) + .filter((message) => message.role === "user") + .at(-1); + assert.ok(secondUserMessage); + const secondAssistant = (manager as any).buildAssistantMessage( + sessionId, + "second answer", + null, + null + ) as SessionMessage; + (manager as any).appendSessionMessage(sessionId, secondAssistant); + + manager.restoreSessionConversation(sessionId, secondUserMessage.id); + + const contents = manager.listSessionMessages(sessionId).map((message) => message.content); + assert.ok(contents.includes("first prompt")); + assert.ok(contents.includes("first answer")); + assert.ok(!contents.includes("second prompt")); + assert.ok(!contents.includes("second answer")); + assert.equal(manager.getSession(sessionId)?.assistantReply, "first answer"); +}); + +test("restoreSessionCode restores project files from the recorded Git checkpoint", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-undo-code-workspace-"); + const home = createTempDir("deepcode-undo-code-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-undo-code"); + const sessionId = "session-code-restore"; + const checkpointHash = createFileHistoryCommit(home, workspace, sessionId, { "tracked.txt": "before\n" }); + createFileHistoryCommit(home, workspace, sessionId, { "tracked.txt": "after\n", "new.txt": "remove me\n" }); + fs.writeFileSync(path.join(workspace, "tracked.txt"), "after\n", "utf8"); + fs.writeFileSync(path.join(workspace, "new.txt"), "remove me\n", "utf8"); + + (manager as any).appendSessionMessage(sessionId, { + ...buildTestMessage("user-with-checkpoint", sessionId, "user", "restore here"), + checkpointHash, + }); + + manager.restoreSessionCode(sessionId, "user-with-checkpoint"); + + assert.equal(fs.readFileSync(path.join(workspace, "tracked.txt"), "utf8"), "before\n"); + assert.equal(fs.existsSync(path.join(workspace, "new.txt")), false); +}); + test("replySession /continue runs trailing pending tool calls before requesting another response", async () => { const workspace = createTempDir("deepcode-continue-tool-workspace-"); const home = createTempDir("deepcode-continue-tool-home-"); @@ -1737,6 +1951,88 @@ test("SessionManager adjusts the active Bash timeout control and session metadat assert.equal(processInfo?.deadlineAt, new Date(timeoutInfo.deadlineAtMs).toISOString()); }); +function hasGit(): boolean { + try { + execFileSync("git", ["--version"], { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function createFileHistoryCommit( + home: string, + workspace: string, + sessionId: string, + files: Record +): string { + const projectCode = workspace.replace(/[\\/]/g, "-").replace(/:/g, ""); + const gitDir = path.join(home, ".deepcode", "projects", projectCode, "file-history", ".git"); + const branchRef = `refs/heads/${sessionId}`; + fs.mkdirSync(path.dirname(gitDir), { recursive: true }); + if (!fs.existsSync(gitDir)) { + runFileHistoryGit(gitDir, workspace, ["init"]); + } + + let parentHash = ""; + try { + parentHash = runFileHistoryGit(gitDir, workspace, ["rev-parse", "--verify", `${branchRef}^{commit}`]).trim(); + } catch { + const emptyTree = runFileHistoryGit(gitDir, workspace, ["mktree"], ""); + parentHash = runFileHistoryGit( + gitDir, + workspace, + ["commit-tree", emptyTree.trim(), "-m", "initial checkpoint"], + "", + fileHistoryCommitEnv() + ).trim(); + runFileHistoryGit(gitDir, workspace, ["update-ref", branchRef, parentHash]); + } + runFileHistoryGit(gitDir, workspace, ["read-tree", "--reset", branchRef]); + + for (const [relativePath, content] of Object.entries(files)) { + const filePath = path.join(workspace, relativePath); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, "utf8"); + } + runFileHistoryGit(gitDir, workspace, ["add", "-f", "-A", "--", ...Object.keys(files)]); + const treeHash = runFileHistoryGit(gitDir, workspace, ["write-tree"]).trim(); + const commitHash = runFileHistoryGit( + gitDir, + workspace, + ["commit-tree", treeHash, "-p", parentHash, "-m", "checkpoint"], + "", + fileHistoryCommitEnv() + ).trim(); + runFileHistoryGit(gitDir, workspace, ["update-ref", branchRef, commitHash, parentHash]); + return commitHash; +} + +function runFileHistoryGit( + gitDir: string, + workspace: string, + args: string[], + input = "", + env: NodeJS.ProcessEnv = process.env +): string { + return execFileSync("git", [`--git-dir=${gitDir}`, `--work-tree=${workspace}`, ...args], { + encoding: "utf8", + input, + env, + stdio: ["pipe", "pipe", "pipe"], + }); +} + +function fileHistoryCommitEnv(): NodeJS.ProcessEnv { + return { + ...process.env, + GIT_AUTHOR_NAME: "DeepCode Test", + GIT_AUTHOR_EMAIL: "deepcode-test@example.com", + GIT_COMMITTER_NAME: "DeepCode Test", + GIT_COMMITTER_EMAIL: "deepcode-test@example.com", + }; +} + function createSessionManager(projectRoot: string, machineId: string): SessionManager { return new SessionManager({ projectRoot, diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts index 34b48d01..30d77eeb 100644 --- a/src/tests/slashCommands.test.ts +++ b/src/tests/slashCommands.test.ts @@ -19,7 +19,18 @@ test("buildSlashCommands prefixes skills before built-ins", () => { assert.equal(items[0].kind, "skill"); assert.equal(items[0].name, "skill-writer"); const builtinNames = items.filter((i) => i.kind !== "skill").map((i) => i.name); - assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "continue", "mcp", "raw", "exit"]); + assert.deepEqual(builtinNames, [ + "skills", + "model", + "new", + "init", + "resume", + "continue", + "undo", + "mcp", + "raw", + "exit", + ]); }); test("filterSlashCommands matches partial prefixes", () => { @@ -66,6 +77,13 @@ test("findExactSlashCommand returns built-in /continue", () => { assert.equal(item?.kind, "continue"); }); +test("findExactSlashCommand returns built-in /undo", () => { + const items = buildSlashCommands(skills); + const item = findExactSlashCommand(items, "/undo"); + assert.ok(item); + assert.equal(item?.kind, "undo"); +}); + test("findExactSlashCommand returns built-in /skills", () => { const items = buildSlashCommands(skills); const item = findExactSlashCommand(items, "/skills"); diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index 29108e5b..454a673b 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -321,7 +321,9 @@ export async function handleEditTool( const updated = applyReplacement(raw, replacementOldString, replacementNewString, matches, replaceAll); const diffPreview = buildDiffPreview(filePath, raw, updated); + context.onBeforeFileMutation?.(filePath); writeTextFile(filePath, updated, metadata.encoding, metadata.lineEndings); + context.onAfterFileMutation?.(filePath); const freshMetadata = readTextFileWithMetadata(filePath); recordFileState( context.sessionId, diff --git a/src/tools/executor.ts b/src/tools/executor.ts index 093e9f3b..73e31f5e 100644 --- a/src/tools/executor.ts +++ b/src/tools/executor.ts @@ -40,6 +40,8 @@ export type ToolExecutionContext = { onProcessExit?: (processId: string | number) => void; onProcessStdout?: (processId: string | number, chunk: string) => void; onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; + onBeforeFileMutation?: (filePath: string) => void; + onAfterFileMutation?: (filePath: string) => void; bashTimeoutMs?: number; bashMinTimeoutMs?: number; }; @@ -49,6 +51,8 @@ export type ToolExecutionHooks = { onProcessExit?: (processId: string | number) => void; onProcessStdout?: (processId: string | number, chunk: string) => void; onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; + onBeforeFileMutation?: (filePath: string) => void; + onAfterFileMutation?: (filePath: string) => void; shouldStop?: () => boolean; }; @@ -217,6 +221,8 @@ export class ToolExecutor { onProcessExit: hooks?.onProcessExit, onProcessStdout: hooks?.onProcessStdout, onProcessTimeoutControl: hooks?.onProcessTimeoutControl, + onBeforeFileMutation: hooks?.onBeforeFileMutation, + onAfterFileMutation: hooks?.onAfterFileMutation, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); diff --git a/src/tools/write-handler.ts b/src/tools/write-handler.ts index 153c1c63..a4c81bf3 100644 --- a/src/tools/write-handler.ts +++ b/src/tools/write-handler.ts @@ -97,7 +97,9 @@ export async function handleWriteTool( const encoding = existingMetadata?.encoding ?? "utf8"; const lineEndings = existingMetadata?.lineEndings ?? (input.content.includes("\r\n") ? "CRLF" : "LF"); const diffPreview = buildDiffPreview(filePath, existingMetadata?.content ?? null, normalizedContent); + context.onBeforeFileMutation?.(filePath); const bytes = writeTextFile(filePath, normalizedContent, encoding, lineEndings); + context.onAfterFileMutation?.(filePath); const freshMetadata = readTextFileWithMetadata(filePath); recordFileState( diff --git a/src/ui/App.tsx b/src/ui/App.tsx index c729dc0e..70f9755a 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -13,6 +13,7 @@ import { type SessionMessage, type SessionStatus, type SkillInfo, + type UndoTarget, type UserPromptContent, } from "../session"; import { @@ -22,9 +23,10 @@ import { type ResolvedDeepcodingSettings, resolveSettingsSources, } from "../settings"; -import { PromptInput, type PromptSubmission } from "./PromptInput"; +import { PromptInput, type PromptDraft, type PromptSubmission } from "./PromptInput"; import { MessageView, RawModeExitPrompt } from "./compoments"; import { SessionList } from "./SessionList"; +import { UndoSelector, type UndoRestoreMode } from "./UndoSelector"; import { buildLoadingText } from "./loadingText"; import { findExpandedThinkingId } from "./thinkingState"; import { WelcomeScreen } from "./WelcomeScreen"; @@ -43,7 +45,7 @@ import { renderMessageToStdout } from "./compoments/MessageView/utils"; const DEFAULT_MODEL = "deepseek-v4-pro"; const DEFAULT_BASE_URL = "https://api.deepseek.com"; -type View = "chat" | "session-list" | "mcp-status"; +type View = "chat" | "session-list" | "undo" | "mcp-status"; type AppProps = { projectRoot: string; @@ -67,6 +69,8 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. const [skills, setSkills] = useState([]); const [messages, setMessages] = useState([]); const [sessions, setSessions] = useState([]); + const [undoTargets, setUndoTargets] = useState([]); + const [promptDraft, setPromptDraft] = useState(null); const [statusLine, setStatusLine] = useState(""); const [errorLine, setErrorLine] = useState(null); const [streamProgress, setStreamProgress] = useState(null); @@ -223,6 +227,17 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setView("session-list"); return; } + if (submission.command === "undo") { + const activeSessionId = sessionManager.getActiveSessionId(); + if (!activeSessionId) { + setErrorLine("No active session to undo."); + return; + } + setShowWelcome(false); + setUndoTargets(sessionManager.listUndoTargets(activeSessionId)); + setView("undo"); + return; + } if (submission.command === "mcp") { setShowWelcome(false); setMcpStatuses(sessionManager.getMcpStatus()); @@ -337,6 +352,20 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. [handlePrompt] ); + const reloadActiveSessionView = useCallback( + (sessionId: string): void => { + process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); + setMessages([]); + setShowWelcome(false); + setWelcomeNonce((n) => n + 1); + setTimeout(() => { + setMessages(loadVisibleMessages(sessionManager, sessionId)); + setShowWelcome(true); + }, 0); + }, + [sessionManager] + ); + useEffect(() => { if (initialPromptSubmittedRef.current || !initialPrompt || !initialPrompt.trim()) { return; @@ -376,6 +405,45 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. [sessionManager, refreshSkills] ); + const handleUndoRestore = useCallback( + async (target: UndoTarget, restoreMode: UndoRestoreMode): Promise => { + const sessionId = sessionManager.getActiveSessionId(); + if (!sessionId) { + setErrorLine("No active session to undo."); + setView("chat"); + setShowWelcome(true); + return; + } + + const errors: string[] = []; + if (restoreMode === "code-and-conversation") { + try { + sessionManager.restoreSessionCode(sessionId, target.message.id); + } catch (error) { + errors.push(`Code restore failed: ${error instanceof Error ? error.message : String(error)}`); + } + } + + let conversationRestored = false; + try { + sessionManager.restoreSessionConversation(sessionId, target.message.id); + conversationRestored = true; + } catch (error) { + errors.push(`Conversation restore failed: ${error instanceof Error ? error.message : String(error)}`); + } + + refreshSessionsList(); + await refreshSkills(sessionId); + setView("chat"); + setErrorLine(errors.length > 0 ? errors.join(" ") : null); + if (conversationRestored) { + setPromptDraft(buildPromptDraftFromSessionMessage(target.message, Date.now())); + } + reloadActiveSessionView(sessionId); + }, + [reloadActiveSessionView, refreshSessionsList, refreshSkills, sessionManager] + ); + const handleRawModeChange = useCallback( (nextMode: string) => { const activeSessionId = sessionManager.getActiveSessionId(); @@ -584,6 +652,15 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. onSelect={(id) => void handleSelectSession(id)} onCancel={() => setView("chat")} /> + ) : view === "undo" ? ( + void handleUndoRestore(target, restoreMode)} + onCancel={() => { + setView("chat"); + setShowWelcome(true); + }} + /> ) : view === "mcp-status" ? ( setView("chat")} /> ) : shouldShowQuestionPrompt && pendingQuestion && !busy ? ( @@ -602,6 +679,7 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. busy={busy} loadingText={loadingText} runningProcesses={runningProcesses} + promptDraft={promptDraft} onSubmit={handleSubmit} onModelConfigChange={handleModelConfigChange} onRawModeChange={handleRawModeChange} @@ -646,6 +724,30 @@ function buildSyntheticUserMessage(content: string, imageCount: number): Session }; } +export function buildPromptDraftFromSessionMessage(message: SessionMessage, nonce: number): PromptDraft { + return { + nonce, + text: typeof message.content === "string" ? message.content : "", + imageUrls: extractImageUrlsFromContentParams(message.contentParams), + }; +} + +function extractImageUrlsFromContentParams(contentParams: unknown): string[] { + const params = Array.isArray(contentParams) ? contentParams : contentParams ? [contentParams] : []; + const imageUrls: string[] = []; + for (const param of params) { + if (!param || typeof param !== "object") { + continue; + } + const record = param as { type?: unknown; image_url?: { url?: unknown } }; + const url = record.image_url?.url; + if (record.type === "image_url" && typeof url === "string" && url) { + imageUrls.push(url); + } + } + return imageUrls; +} + function isCurrentSessionEmpty(sessionManager: SessionManager): boolean { const activeSessionId = sessionManager.getActiveSessionId(); return !activeSessionId || !sessionManager.getSession(activeSessionId); diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index b35f72e9..ffb614d2 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -57,7 +57,13 @@ export type PromptSubmission = { text: string; imageUrls: string[]; selectedSkills?: SkillInfo[]; - command?: "new" | "resume" | "continue" | "mcp" | "exit"; + command?: "new" | "resume" | "continue" | "undo" | "mcp" | "exit"; +}; + +export type PromptDraft = { + nonce: number; + text: string; + imageUrls: string[]; }; type Props = { @@ -71,6 +77,7 @@ type Props = { disabled?: boolean; placeholder?: string; runningProcesses?: SessionEntry["processes"]; + promptDraft?: PromptDraft | null; onSubmit: (submission: PromptSubmission) => void; onModelConfigChange: (selection: ModelConfigSelection) => string | Promise; onRawModeChange?: (mode: string) => void; @@ -124,6 +131,7 @@ export const PromptInput = React.memo(function PromptInput({ disabled, placeholder, runningProcesses, + promptDraft, onSubmit, onModelConfigChange, onInterrupt, @@ -154,6 +162,7 @@ export const PromptInput = React.memo(function PromptInput({ const undoRedoRef = React.useRef(createPromptUndoRedoState()); const wasBusyRef = React.useRef(busy); const hadFileMentionTokenRef = React.useRef(false); + const appliedDraftNonceRef = React.useRef(null); const fileMentionToken = getCurrentFileMentionToken(buffer); const hasFileMentionToken = fileMentionToken !== null; @@ -266,6 +275,22 @@ export const PromptInput = React.memo(function PromptInput({ return () => clearTimeout(timer); }, [statusMessage]); + useEffect(() => { + if (!promptDraft || appliedDraftNonceRef.current === promptDraft.nonce) { + return; + } + appliedDraftNonceRef.current = promptDraft.nonce; + setBuffer({ text: promptDraft.text, cursor: promptDraft.text.length }); + setImageUrls(promptDraft.imageUrls); + setSelectedSkills([]); + setShowSkillsDropdown(false); + setOpenRawModelDropdown(false); + setModelDropdownStep(null); + setHistoryCursor(-1); + setDraftBeforeHistory(null); + clearPromptUndoRedoState(undoRedoRef.current); + }, [promptDraft]); + useEffect(() => { setHistoryCursor(-1); setDraftBeforeHistory(null); @@ -766,6 +791,15 @@ export const PromptInput = React.memo(function PromptInput({ setShowSkillsDropdown(false); return; } + if (item.kind === "undo") { + onSubmit({ text: "/undo", imageUrls: [], command: "undo" }); + setBuffer(EMPTY_BUFFER); + clearUndoRedoStacks(); + setImageUrls([]); + setSelectedSkills([]); + setShowSkillsDropdown(false); + return; + } if (item.kind === "mcp") { onSubmit({ text: "/mcp", imageUrls: [], command: "mcp" }); setBuffer(EMPTY_BUFFER); diff --git a/src/ui/UndoSelector.tsx b/src/ui/UndoSelector.tsx new file mode 100644 index 00000000..fad3e178 --- /dev/null +++ b/src/ui/UndoSelector.tsx @@ -0,0 +1,195 @@ +import React, { useMemo, useState } from "react"; +import { Box, Text, useInput, useWindowSize } from "ink"; +import type { UndoTarget } from "../session"; + +export type UndoRestoreMode = "code-and-conversation" | "conversation"; + +type Props = { + targets: UndoTarget[]; + onSelect: (target: UndoTarget, mode: UndoRestoreMode) => void; + onCancel: () => void; +}; + +type Phase = "message" | "mode"; + +const MAX_VISIBLE_TARGETS = 7; + +export function UndoSelector({ targets, onSelect, onCancel }: Props): React.ReactElement { + const [phase, setPhase] = useState("message"); + const [targetIndex, setTargetIndex] = useState(Math.max(0, targets.length - 1)); + const [modeIndex, setModeIndex] = useState(0); + const { columns, rows } = useWindowSize(); + + const safeTargetIndex = useMemo(() => { + if (targets.length === 0) { + return 0; + } + return Math.max(0, Math.min(targetIndex, targets.length - 1)); + }, [targetIndex, targets.length]); + + const selectedTarget = targets[safeTargetIndex] ?? null; + const maxVisible = Math.max(1, Math.min(MAX_VISIBLE_TARGETS, rows - 8)); + const scrollOffset = Math.max(0, Math.min(safeTargetIndex - Math.floor(maxVisible / 2), targets.length - maxVisible)); + const visibleTargets = targets.slice(scrollOffset, scrollOffset + maxVisible); + + useInput((input, key) => { + if (key.escape || (key.ctrl && (input === "c" || input === "C"))) { + if (phase === "mode") { + setPhase("message"); + return; + } + onCancel(); + return; + } + + if (targets.length === 0) { + return; + } + + if (phase === "message") { + if (key.upArrow) { + setTargetIndex((index) => Math.max(0, index - 1)); + return; + } + if (key.downArrow) { + setTargetIndex((index) => Math.min(targets.length - 1, index + 1)); + return; + } + if (key.home) { + setTargetIndex(0); + return; + } + if (key.end) { + setTargetIndex(targets.length - 1); + return; + } + if (key.return) { + setModeIndex(selectedTarget?.canRestoreCode ? 0 : 1); + setPhase("mode"); + } + return; + } + + if (key.upArrow || key.downArrow) { + setModeIndex((index) => (index === 0 ? 1 : 0)); + return; + } + if (key.return && selectedTarget) { + onSelect(selectedTarget, modeIndex === 0 ? "code-and-conversation" : "conversation"); + } + }); + + if (targets.length === 0) { + return ( + + Nothing to undo yet. + Press Esc to go back. + + ); + } + + return ( + + + + + Undo + + restore to the point before a prompt + + {phase === "message" ? ( + + {visibleTargets.map((target, visibleIndex) => { + const actualIndex = scrollOffset + visibleIndex; + const isActive = actualIndex === safeTargetIndex; + return ( + + {isActive ? "> " : " "} + + + {formatUndoMessage(target.message.content)} + + + {formatTimestamp(target.message.createTime)} + {target.canRestoreCode ? " · code checkpoint available" : " · conversation only"} + + + + ); + })} + + ) : ( + + Selected prompt: + {formatUndoMessage(selectedTarget?.message.content ?? "")} + + + {modeIndex === 0 ? "> " : " "}Restore code and conversation + + + {" "} + {selectedTarget?.canRestoreCode + ? "Restore files from the recorded Git checkpoint, then fork the conversation." + : "No code checkpoint is recorded for this prompt."} + + + {modeIndex === 1 ? "> " : " "}Restore conversation + + {" "}Fork the conversation without changing files. + + + )} + + + {phase === "message" + ? "↑/↓ navigate · Enter choose · Esc cancel" + : "↑/↓ choose restore mode · Enter restore · Esc back"} + + + + + ); +} + +function formatUndoMessage(content: unknown): string { + const text = typeof content === "string" && content.trim() ? content.trim() : "(empty message)"; + const singleLine = text.replace(/\r?\n/g, " ").replace(/\s+/g, " "); + return singleLine.length > 90 ? `${singleLine.slice(0, 89)}…` : singleLine; +} + +function formatTimestamp(value: string): string { + const date = new Date(value); + if (Number.isNaN(date.valueOf())) { + return value; + } + return date.toLocaleString(); +} diff --git a/src/ui/index.ts b/src/ui/index.ts index f2e698c1..656b582e 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -6,6 +6,7 @@ export { writeModelConfigSelection, resolveCurrentSettings, createOpenAIClient, + buildPromptDraftFromSessionMessage, } from "./App"; export { default as AppContainer } from "./AppContainer"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; @@ -30,6 +31,7 @@ export { useTerminalInput, parseTerminalInput, type PromptSubmission, + type PromptDraft, type InputKey, } from "./PromptInput"; export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./prompt/cursor"; diff --git a/src/ui/slashCommands.ts b/src/ui/slashCommands.ts index 948a7abd..6d9b7cc1 100644 --- a/src/ui/slashCommands.ts +++ b/src/ui/slashCommands.ts @@ -8,6 +8,7 @@ export type SlashCommandKind = | "init" | "resume" | "continue" + | "undo" | "mcp" | "raw" | "exit"; @@ -58,6 +59,12 @@ export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [ label: "/continue", description: "Continue the active conversation or pick one to resume", }, + { + kind: "undo", + name: "undo", + label: "/undo", + description: "Restore code and/or conversation to a previous point", + }, { kind: "mcp", name: "mcp", From 883f1fd051a648b67b457288580565b6f7c7aca0 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 20 May 2026 11:46:52 +0800 Subject: [PATCH 033/212] refactor: extract the file-history checkpoint logic out of SessionManager --- src/common/file-history.ts | 194 ++++++++++++++++++++++++++++++++++++ src/session.ts | 196 +++---------------------------------- 2 files changed, 209 insertions(+), 181 deletions(-) create mode 100644 src/common/file-history.ts diff --git a/src/common/file-history.ts b/src/common/file-history.ts new file mode 100644 index 00000000..5194e6ec --- /dev/null +++ b/src/common/file-history.ts @@ -0,0 +1,194 @@ +import * as childProcess from "child_process"; +import * as fs from "fs"; +import * as path from "path"; + +const FILE_HISTORY_AUTHOR_NAME = "DeepCode Checkpoint"; +const FILE_HISTORY_AUTHOR_EMAIL = "deepcode-checkpoint@localhost"; + +export class GitFileHistory { + constructor( + private readonly projectRoot: string, + private readonly gitDir: string + ) {} + + ensureSession(sessionId: string): string | undefined { + const branchRef = this.getSessionBranchRef(sessionId); + if (!branchRef) { + return undefined; + } + + try { + if (!fs.existsSync(this.gitDir)) { + fs.mkdirSync(path.dirname(this.gitDir), { recursive: true }); + this.runGit(["init"], { includeWorkTree: true }); + } + + const current = this.getCurrentCheckpointHash(sessionId); + if (current) { + return current; + } + + const emptyTree = this.runGit(["mktree"], { includeWorkTree: false, input: "" }).trim(); + const commitHash = this.createCommit(emptyTree, null, "Initial checkpoint"); + this.runGit(["update-ref", branchRef, commitHash], { includeWorkTree: false }); + return commitHash; + } catch { + return undefined; + } + } + + getCurrentCheckpointHash(sessionId: string): string | undefined { + const branchRef = this.getSessionBranchRef(sessionId); + if (!branchRef || !fs.existsSync(this.gitDir)) { + return undefined; + } + + try { + const hash = this.runGit(["rev-parse", "--verify", `${branchRef}^{commit}`], { + includeWorkTree: false, + }).trim(); + return isCommitHash(hash) ? hash : undefined; + } catch { + return undefined; + } + } + + recordCheckpoint(sessionId: string, filePaths: string[], message: string): string | undefined { + const branchRef = this.getSessionBranchRef(sessionId); + if (!branchRef) { + return undefined; + } + + const relativePaths = filePaths + .map((filePath) => this.toProjectRelativeGitPath(filePath)) + .filter((filePath): filePath is string => Boolean(filePath)); + if (relativePaths.length === 0) { + return this.getCurrentCheckpointHash(sessionId); + } + + try { + const parentHash = this.ensureSession(sessionId); + if (!parentHash) { + return undefined; + } + this.runGit(["read-tree", "--reset", branchRef], { includeWorkTree: true }); + this.runGit(["add", "-f", "-A", "--", ...relativePaths], { includeWorkTree: true }); + const treeHash = this.runGit(["write-tree"], { includeWorkTree: false }).trim(); + const parentTreeHash = this.runGit(["rev-parse", `${parentHash}^{tree}`], { + includeWorkTree: false, + }).trim(); + if (treeHash === parentTreeHash) { + return parentHash; + } + + const commitHash = this.createCommit(treeHash, parentHash, message); + this.runGit(["update-ref", branchRef, commitHash, parentHash], { includeWorkTree: false }); + return commitHash; + } catch { + return undefined; + } + } + + canRestore(sessionId: string, checkpointHash: string): boolean { + if (!isCommitHash(checkpointHash)) { + return false; + } + if (!this.getSessionBranchRef(sessionId)) { + return false; + } + if (!fs.existsSync(this.gitDir)) { + return false; + } + + try { + this.runGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false }); + return true; + } catch { + return false; + } + } + + restore(sessionId: string, checkpointHash: string): void { + if (!isCommitHash(checkpointHash)) { + throw new Error("Invalid checkpoint hash."); + } + const branchRef = this.getSessionBranchRef(sessionId); + if (!branchRef || !fs.existsSync(this.gitDir)) { + throw new Error("File history Git repository was not found for this project."); + } + this.runGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false }); + + try { + this.runGit(["read-tree", "--reset", branchRef], { includeWorkTree: true }); + } catch { + // If the session branch is missing, fall back to the target tree only. + // The target checkpoint has already been validated above. + } + this.runGit(["read-tree", "--reset", "-u", checkpointHash], { includeWorkTree: true }); + this.runGit(["update-ref", branchRef, checkpointHash], { includeWorkTree: false }); + } + + private getSessionBranchRef(sessionId: string): string | null { + if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) { + return null; + } + return `refs/heads/${sessionId}`; + } + + private createCommit(treeHash: string, parentHash: string | null, message: string): string { + const args = ["commit-tree", treeHash]; + if (parentHash) { + args.push("-p", parentHash); + } + args.push("-m", message); + return this.runGit(args, { + includeWorkTree: false, + env: getFileHistoryGitEnv(), + }).trim(); + } + + private toProjectRelativeGitPath(filePath: string): string | null { + const absolutePath = path.resolve(filePath); + const relativePath = path.relative(this.projectRoot, absolutePath); + if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) { + return null; + } + return relativePath.split(path.sep).join("/"); + } + + private runGit( + args: string[], + options: { includeWorkTree: boolean; input?: string; env?: NodeJS.ProcessEnv } + ): string { + const gitArgs = [`--git-dir=${this.gitDir}`]; + if (options.includeWorkTree) { + gitArgs.push(`--work-tree=${this.projectRoot}`); + } + gitArgs.push(...args); + const result = childProcess.spawnSync("git", gitArgs, { + encoding: "utf8", + input: options.input, + env: options.env, + stdio: ["pipe", "pipe", "pipe"], + }); + if (result.status !== 0) { + const detail = (result.stderr || result.stdout || "").trim(); + throw new Error(detail || `git ${args.join(" ")} failed`); + } + return result.stdout ?? ""; + } +} + +function getFileHistoryGitEnv(): NodeJS.ProcessEnv { + return { + ...process.env, + GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || FILE_HISTORY_AUTHOR_NAME, + GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || FILE_HISTORY_AUTHOR_EMAIL, + GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || FILE_HISTORY_AUTHOR_NAME, + GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || FILE_HISTORY_AUTHOR_EMAIL, + }; +} + +function isCommitHash(value: string): boolean { + return /^[0-9a-f]{40}$/i.test(value); +} diff --git a/src/session.ts b/src/session.ts index 6b2ceee0..88e85b69 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2,7 +2,6 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; -import * as childProcess from "child_process"; import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; @@ -29,14 +28,13 @@ import type { McpServerConfig } from "./settings"; import { logApiError } from "./common/error-logger"; import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; import { killProcessTree } from "./common/process-tree"; +import { GitFileHistory } from "./common/file-history"; const MAX_SESSION_ENTRIES = 50; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; -const FILE_HISTORY_AUTHOR_NAME = "DeepCode Checkpoint"; -const FILE_HISTORY_AUTHOR_EMAIL = "deepcode-checkpoint@localhost"; type ChatCompletionDebugOptions = { enabled?: boolean; @@ -1585,113 +1583,40 @@ ${skillMd} return { projectCode, projectDir, sessionsIndexPath }; } + private getFileHistory(): GitFileHistory { + return new GitFileHistory(this.projectRoot, this.getFileHistoryGitDir()); + } + private getFileHistoryGitDir(): string { const { projectDir } = this.getProjectStorage(); return path.join(projectDir, "file-history", ".git"); } - private getSessionBranchRef(sessionId: string): string | null { - if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) { - return null; - } - return `refs/heads/${sessionId}`; - } - private ensureFileHistorySession(sessionId: string): string | undefined { - const branchRef = this.getSessionBranchRef(sessionId); - if (!branchRef) { - return undefined; - } - - try { - const gitDir = this.getFileHistoryGitDir(); - if (!fs.existsSync(gitDir)) { - fs.mkdirSync(path.dirname(gitDir), { recursive: true }); - this.runFileHistoryGit(["init"], { includeWorkTree: true }); - } - - const current = this.getCurrentCheckpointHash(sessionId); - if (current) { - return current; - } - - const emptyTree = this.runFileHistoryGit(["mktree"], { includeWorkTree: false, input: "" }).trim(); - const commitHash = this.createFileHistoryCommit(emptyTree, null, "Initial checkpoint"); - this.runFileHistoryGit(["update-ref", branchRef, commitHash], { includeWorkTree: false }); - return commitHash; - } catch { - return undefined; - } + return this.getFileHistory().ensureSession(sessionId); } private getCurrentCheckpointHash(sessionId: string): string | undefined { - const gitDir = this.getFileHistoryGitDir(); - const branchRef = this.getSessionBranchRef(sessionId); - if (!branchRef || !fs.existsSync(gitDir)) { - return undefined; - } - - try { - const hash = this.runFileHistoryGit(["rev-parse", "--verify", `${branchRef}^{commit}`], { - includeWorkTree: false, - }).trim(); - return this.isCommitHash(hash) ? hash : undefined; - } catch { - return undefined; - } + return this.getFileHistory().getCurrentCheckpointHash(sessionId); } private prepareFileMutationCheckpoint(sessionId: string, filePath: string): void { - const previousHash = this.ensureFileHistorySession(sessionId); + const fileHistory = this.getFileHistory(); + const previousHash = fileHistory.ensureSession(sessionId); if (!previousHash) { return; } this.updateLatestUserCheckpointHash(sessionId, undefined, previousHash); - const nextHash = this.recordFileHistoryCheckpoint(sessionId, [filePath], "Pre-mutation checkpoint"); + const nextHash = fileHistory.recordCheckpoint(sessionId, [filePath], "Pre-mutation checkpoint"); if (nextHash && nextHash !== previousHash) { this.updateLatestUserCheckpointHash(sessionId, previousHash, nextHash); } } private recordFileMutationCheckpoint(sessionId: string, filePath: string): void { - this.ensureFileHistorySession(sessionId); - this.recordFileHistoryCheckpoint(sessionId, [filePath], "File mutation checkpoint"); - } - - private recordFileHistoryCheckpoint(sessionId: string, filePaths: string[], message: string): string | undefined { - const branchRef = this.getSessionBranchRef(sessionId); - if (!branchRef) { - return undefined; - } - - const relativePaths = filePaths - .map((filePath) => this.toProjectRelativeGitPath(filePath)) - .filter((filePath): filePath is string => Boolean(filePath)); - if (relativePaths.length === 0) { - return this.getCurrentCheckpointHash(sessionId); - } - - try { - const parentHash = this.ensureFileHistorySession(sessionId); - if (!parentHash) { - return undefined; - } - this.runFileHistoryGit(["read-tree", "--reset", branchRef], { includeWorkTree: true }); - this.runFileHistoryGit(["add", "-f", "-A", "--", ...relativePaths], { includeWorkTree: true }); - const treeHash = this.runFileHistoryGit(["write-tree"], { includeWorkTree: false }).trim(); - const parentTreeHash = this.runFileHistoryGit(["rev-parse", `${parentHash}^{tree}`], { - includeWorkTree: false, - }).trim(); - if (treeHash === parentTreeHash) { - return parentHash; - } - - const commitHash = this.createFileHistoryCommit(treeHash, parentHash, message); - this.runFileHistoryGit(["update-ref", branchRef, commitHash, parentHash], { includeWorkTree: false }); - return commitHash; - } catch { - return undefined; - } + const fileHistory = this.getFileHistory(); + fileHistory.ensureSession(sessionId); + fileHistory.recordCheckpoint(sessionId, [filePath], "File mutation checkpoint"); } private updateLatestUserCheckpointHash(sessionId: string, previousHash: string | undefined, nextHash: string): void { @@ -1714,103 +1639,12 @@ ${skillMd} } } - private createFileHistoryCommit(treeHash: string, parentHash: string | null, message: string): string { - const args = ["commit-tree", treeHash]; - if (parentHash) { - args.push("-p", parentHash); - } - args.push("-m", message); - return this.runFileHistoryGit(args, { - includeWorkTree: false, - env: this.getFileHistoryGitEnv(), - }).trim(); - } - - private getFileHistoryGitEnv(): NodeJS.ProcessEnv { - return { - ...process.env, - GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || FILE_HISTORY_AUTHOR_NAME, - GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || FILE_HISTORY_AUTHOR_EMAIL, - GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || FILE_HISTORY_AUTHOR_NAME, - GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || FILE_HISTORY_AUTHOR_EMAIL, - }; - } - - private toProjectRelativeGitPath(filePath: string): string | null { - const absolutePath = path.resolve(filePath); - const relativePath = path.relative(this.projectRoot, absolutePath); - if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) { - return null; - } - return relativePath.split(path.sep).join("/"); - } - private canRestoreCheckpointHash(sessionId: string, checkpointHash: string): boolean { - if (!this.isCommitHash(checkpointHash)) { - return false; - } - if (!this.getSessionBranchRef(sessionId)) { - return false; - } - const gitDir = this.getFileHistoryGitDir(); - if (!fs.existsSync(gitDir)) { - return false; - } - - try { - this.runFileHistoryGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false }); - return true; - } catch { - return false; - } + return this.getFileHistory().canRestore(sessionId, checkpointHash); } private restoreCheckpointHash(sessionId: string, checkpointHash: string): void { - if (!this.isCommitHash(checkpointHash)) { - throw new Error("Invalid checkpoint hash."); - } - const gitDir = this.getFileHistoryGitDir(); - const branchRef = this.getSessionBranchRef(sessionId); - if (!branchRef || !fs.existsSync(gitDir)) { - throw new Error("File history Git repository was not found for this project."); - } - this.runFileHistoryGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false }); - - try { - this.runFileHistoryGit(["read-tree", "--reset", branchRef], { includeWorkTree: true }); - } catch { - // If the session branch is missing, fall back to the target tree only. - // The target checkpoint has already been validated above. - } - this.runFileHistoryGit(["read-tree", "--reset", "-u", checkpointHash], { includeWorkTree: true }); - this.runFileHistoryGit(["update-ref", branchRef, checkpointHash], { includeWorkTree: false }); - } - - private runFileHistoryGit( - args: string[], - options: { includeWorkTree: boolean; input?: string; env?: NodeJS.ProcessEnv } - ): string { - const gitDir = this.getFileHistoryGitDir(); - const gitArgs = [`--git-dir=${gitDir}`]; - if (options.includeWorkTree) { - gitArgs.push(`--work-tree=${this.projectRoot}`); - } - gitArgs.push(...args); - const result = childProcess.spawnSync("git", gitArgs, { - encoding: "utf8", - input: options.input, - env: options.env, - stdio: ["pipe", "pipe", "pipe"], - }); - if (result.status !== 0) { - const detail = (result.stderr || result.stdout || "").trim(); - throw new Error(detail || `git ${args.join(" ")} failed`); - } - return result.stdout ?? ""; - } - - private isCommitHash(value: string): boolean { - return /^[0-9a-f]{40}$/i.test(value); + this.getFileHistory().restore(sessionId, checkpointHash); } private isUndoTargetMessage(message: SessionMessage): boolean { From 057e3538b286ca5f67a4968674014baf0dbdc808 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 20 May 2026 12:03:31 +0800 Subject: [PATCH 034/212] fix: Bash timeout session test now uses a temporary HOME --- src/tests/session.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index c02c0fa3..3658e1c0 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1919,6 +1919,9 @@ test("SessionManager treats OpenAI APIUserAbortError as interrupted", async () = test("SessionManager adjusts the active Bash timeout control and session metadata", async () => { const workspace = createTempDir("deepcode-bash-timeout-session-"); + const home = createTempDir("deepcode-bash-timeout-home-"); + setHomeDir(home); + const manager = createSessionManager(workspace, ""); const sessionId = await manager.createSession({ text: "hello" }); From f37ee2b71c1320820bd0e9b970a45fea4815556f Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 14:00:28 +0800 Subject: [PATCH 035/212] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=8F=90=E5=8F=8A=E8=8F=9C=E5=8D=95=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96PromptInput=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 FileMentionMenu 组件用于文件提及功能显示与交互 - 在 components/index.ts 中导出 FileMentionMenu 组件 - PromptInput 集成 FileMentionMenu 替换原 DropdownMenu 实现 - 重构 PromptInput 相关状态管理及事件处理,移除冗余索引状态 - 提取 resetPromptInput 函数简化重置输入框逻辑 - 优化键盘事件处理逻辑,简化文件提及菜单快捷键支持 - 使用 FileMentionMenu 替换内联菜单渲染,提高代码复用性与可维护性 --- src/ui/PromptInput.tsx | 129 +++++--------------- src/ui/components/FileMentionMenu/index.tsx | 117 ++++++++++++++++++ src/ui/components/index.ts | 1 + 3 files changed, 147 insertions(+), 100 deletions(-) create mode 100644 src/ui/components/FileMentionMenu/index.tsx diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index a79fe304..44e1754c 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -50,8 +50,7 @@ import type { InputKey } from "./prompt"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, useTerminalFocusReporting } from "./prompt"; import SlashCommandMenu from "./SlashCommandMenu"; import type { ModelConfigSelection } from "../settings"; -import DropdownMenu from "./DropdownMenu"; -import { ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; +import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; export type PromptSubmission = { text: string; @@ -127,7 +126,6 @@ export const PromptInput = React.memo(function PromptInput({ const [openRawModelDropdown, setOpenRawModelDropdown] = useState(false); const [showModelDropdown, setShowModelDropdown] = useState(false); const [fileMentionItems, setFileMentionItems] = useState(() => scanFileMentionItems(projectRoot)); - const [fileMentionIndex, setFileMentionIndex] = useState(0); const [dismissedFileMentionKey, setDismissedFileMentionKey] = useState(null); const [historyCursor, setHistoryCursor] = useState(-1); const [draftBeforeHistory, setDraftBeforeHistory] = useState(null); @@ -213,16 +211,6 @@ export const PromptInput = React.memo(function PromptInput({ } }, [fileMentionKey]); - useEffect(() => { - if (!showFileMentionMenu) { - setFileMentionIndex(0); - return; - } - if (fileMentionIndex >= fileMentionMatches.length) { - setFileMentionIndex(Math.max(0, fileMentionMatches.length - 1)); - } - }, [fileMentionMatches.length, fileMentionIndex, showFileMentionMenu]); - useEffect(() => { if (!statusMessage) { return; @@ -252,8 +240,7 @@ export const PromptInput = React.memo(function PromptInput({ } if (key.escape) { - if (showFileMentionMenu && fileMentionKey) { - setDismissedFileMentionKey(fileMentionKey); + if (showFileMentionMenu) { return; } if (busy) { @@ -345,32 +332,9 @@ export const PromptInput = React.memo(function PromptInput({ const isPlainReturn = returnAction === "submit"; if (showFileMentionMenu) { - if (key.upArrow) { - if (fileMentionMatches.length > 0) { - setFileMentionIndex((idx) => (idx - 1 + fileMentionMatches.length) % fileMentionMatches.length); - } + if (key.upArrow || key.downArrow || key.tab || returnAction === "submit") { return; } - if (key.downArrow) { - if (fileMentionMatches.length > 0) { - setFileMentionIndex((idx) => (idx + 1) % fileMentionMatches.length); - } - return; - } - if (key.tab || returnAction === "submit") { - const selected = fileMentionMatches[fileMentionIndex]; - if (selected && fileMentionToken) { - insertFileMentionSelection(selected); - return; - } - if (key.tab) { - setDismissedFileMentionKey(fileMentionKey); - return; - } - if (fileMentionKey) { - setDismissedFileMentionKey(fileMentionKey); - } - } } if (showMenu) { @@ -613,6 +577,14 @@ export const PromptInput = React.memo(function PromptInput({ setDismissedFileMentionKey(null); } + function resetPromptInput(): void { + setBuffer(EMPTY_BUFFER); + clearUndoRedoStacks(); + setImageUrls([]); + setSelectedSkills([]); + setShowSkillsDropdown(false); + } + function handleSlashSelection(item: SlashCommandItem): void { if (busy && item.kind !== "exit") { setStatusMessage("wait for the current response or press esc to interrupt"); @@ -643,47 +615,27 @@ export const PromptInput = React.memo(function PromptInput({ } if (item.kind === "new") { onSubmit({ text: "", imageUrls: [], command: "new" }); - setBuffer(EMPTY_BUFFER); - clearUndoRedoStacks(); - setImageUrls([]); - setSelectedSkills([]); - setShowSkillsDropdown(false); + resetPromptInput(); return; } if (item.kind === "init") { onSubmit(buildInitPromptSubmission(selectedSkills)); - setBuffer(EMPTY_BUFFER); - clearUndoRedoStacks(); - setImageUrls([]); - setSelectedSkills([]); - setShowSkillsDropdown(false); + resetPromptInput(); return; } if (item.kind === "resume") { onSubmit({ text: "", imageUrls: [], command: "resume" }); - setBuffer(EMPTY_BUFFER); - clearUndoRedoStacks(); - setImageUrls([]); - setSelectedSkills([]); - setShowSkillsDropdown(false); + resetPromptInput(); return; } if (item.kind === "continue") { onSubmit({ text: "/continue", imageUrls: [], command: "continue" }); - setBuffer(EMPTY_BUFFER); - clearUndoRedoStacks(); - setImageUrls([]); - setSelectedSkills([]); - setShowSkillsDropdown(false); + resetPromptInput(); return; } if (item.kind === "mcp") { onSubmit({ text: "/mcp", imageUrls: [], command: "mcp" }); - setBuffer(EMPTY_BUFFER); - clearUndoRedoStacks(); - setImageUrls([]); - setSelectedSkills([]); - setShowSkillsDropdown(false); + resetPromptInput(); return; } if (item.kind === "exit") { @@ -718,11 +670,7 @@ export const PromptInput = React.memo(function PromptInput({ imageUrls, selectedSkills, }); - setBuffer(EMPTY_BUFFER); - clearUndoRedoStacks(); - setImageUrls([]); - setSelectedSkills([]); - setShowSkillsDropdown(false); + resetPromptInput(); } function addSelectedSkill(skill: SkillInfo): void { @@ -798,37 +746,18 @@ export const PromptInput = React.memo(function PromptInput({ onModelConfigChange={onModelConfigChange} onStatusMessage={setStatusMessage} /> - {showFileMentionMenu ? ( - ({ - key: item.path, - label: item.path, - description: item.type === "directory" ? "directory" : "file", - }))} - activeIndex={fileMentionIndex} - activeColor="#229ac3" - maxVisible={8} - renderItem={(item, isActive) => ( - - {isActive ? "> " : " "} - - - {item.label} - - - {item.description ? ( - - {item.description} - - ) : null} - - )} - /> - ) : null} + { + if (fileMentionKey) { + setDismissedFileMentionKey(fileMentionKey); + } + }} + onSelect={insertFileMentionSelection} + /> {!showFooterText && ( diff --git a/src/ui/components/FileMentionMenu/index.tsx b/src/ui/components/FileMentionMenu/index.tsx new file mode 100644 index 00000000..ce9a8ee8 --- /dev/null +++ b/src/ui/components/FileMentionMenu/index.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from "react"; +import { Box, Text } from "ink"; +import { useInput } from "ink"; +import DropdownMenu from "../../DropdownMenu"; +import type { FileMentionItem, FileMentionToken } from "../../fileMentions"; + +type Props = { + open: boolean; + width: number; + token: FileMentionToken | null; + items: FileMentionItem[]; + onClose: () => void; + onSelect: (item: FileMentionItem) => void; +}; + +const FileMentionMenu: React.FC = ({ open, width, token, items, onClose, onSelect }) => { + const [activeIndex, setActiveIndex] = useState(0); + + // Reset index when opened + useEffect(() => { + if (open) { + setActiveIndex(0); + } + }, [open]); + + // Validate activeIndex bounds + useEffect(() => { + if (!open) { + return; + } + if (items.length === 0) { + setActiveIndex(0); + return; + } + if (activeIndex >= items.length) { + setActiveIndex(Math.max(0, items.length - 1)); + } + }, [activeIndex, items.length, open]); + + useInput( + (input, key) => { + if (!open) { + return; + } + + if (key.escape) { + onClose(); + return; + } + + if (key.upArrow) { + if (items.length > 0) { + setActiveIndex((idx) => (idx - 1 + items.length) % items.length); + } + return; + } + + if (key.downArrow) { + if (items.length > 0) { + setActiveIndex((idx) => (idx + 1) % items.length); + } + return; + } + + if (key.tab || (key.return && !key.shift && !key.meta)) { + const selected = items[activeIndex]; + if (selected) { + onSelect(selected); + return; + } + if (key.tab) { + onClose(); + } + return; + } + }, + { isActive: open } + ); + + if (!open) { + return null; + } + + return ( + ({ + key: item.path, + label: item.path, + description: item.type === "directory" ? "directory" : "file", + }))} + activeIndex={activeIndex} + activeColor="#229ac3" + maxVisible={8} + renderItem={(item, isActive) => ( + + {isActive ? "> " : " "} + + + {item.label} + + + {item.description ? ( + + {item.description} + + ) : null} + + )} + /> + ); +}; + +export default FileMentionMenu; diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index 1d929f36..635f733c 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -3,3 +3,4 @@ export { MessageView } from "./MessageView"; export { RawModeExitPrompt } from "./RawModeExitPrompt"; export { default as SkillsDropdown } from "./SkillsDropdown"; export { default as ModelsDropdown } from "./ModelsDropdown"; +export { default as FileMentionMenu } from "./FileMentionMenu"; From f611c948fd117c105a7610ca0b8700e181c95140 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 20 May 2026 14:15:30 +0800 Subject: [PATCH 036/212] fix: update git command options to handle line endings consistently --- src/common/file-history.ts | 2 +- src/tests/session.test.ts | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/common/file-history.ts b/src/common/file-history.ts index 5194e6ec..d5966d94 100644 --- a/src/common/file-history.ts +++ b/src/common/file-history.ts @@ -160,7 +160,7 @@ export class GitFileHistory { args: string[], options: { includeWorkTree: boolean; input?: string; env?: NodeJS.ProcessEnv } ): string { - const gitArgs = [`--git-dir=${this.gitDir}`]; + const gitArgs = ["-c", "core.autocrlf=false", "-c", "core.eol=lf", `--git-dir=${this.gitDir}`]; if (options.includeWorkTree) { gitArgs.push(`--work-tree=${this.projectRoot}`); } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 3658e1c0..d5191fac 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -2018,12 +2018,16 @@ function runFileHistoryGit( input = "", env: NodeJS.ProcessEnv = process.env ): string { - return execFileSync("git", [`--git-dir=${gitDir}`, `--work-tree=${workspace}`, ...args], { - encoding: "utf8", - input, - env, - stdio: ["pipe", "pipe", "pipe"], - }); + return execFileSync( + "git", + ["-c", "core.autocrlf=false", "-c", "core.eol=lf", `--git-dir=${gitDir}`, `--work-tree=${workspace}`, ...args], + { + encoding: "utf8", + input, + env, + stdio: ["pipe", "pipe", "pipe"], + } + ); } function fileHistoryCommitEnv(): NodeJS.ProcessEnv { From db6f0c6991095b194f224988e1a72d769f860483 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 14:24:59 +0800 Subject: [PATCH 037/212] =?UTF-8?q?feat(SessionList):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E8=BF=87=E6=BB=A4=E5=8A=9F=E8=83=BD=E5=8F=8A?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增filterSessions函数支持根据关键词过滤会话,包括摘要、状态、失败原因和助手回复 - 支持搜索查询框,动态过滤并显示匹配的会话列表 - 实现格式化会话状态显示,如"completed"显示为"done" - 搜索过程中支持编辑和清除查询内容,按Esc键清除搜索或退出 - 会话列表根据搜索结果自动调整高亮和滚动 - 增强交互提示,根据搜索状态显示不同的快捷键说明 - 针对新增功能添加了大量单元测试覆盖各种匹配和边界情况 - 导出filterSessions和formatSessionStatus以供外部使用 --- src/tests/sessionList.test.ts | 108 +++++++++++++++- src/ui/SessionList.tsx | 227 ++++++++++++++++++++++++++-------- src/ui/index.ts | 2 +- 3 files changed, 280 insertions(+), 57 deletions(-) diff --git a/src/tests/sessionList.test.ts b/src/tests/sessionList.test.ts index e3bf51ce..3dfda332 100644 --- a/src/tests/sessionList.test.ts +++ b/src/tests/sessionList.test.ts @@ -1,6 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { formatSessionTitle } from "../ui"; +import { formatSessionTitle, filterSessions, formatSessionStatus } from "../ui"; +import type { SessionEntry } from "../session"; test("formatSessionTitle replaces newlines with spaces", () => { assert.equal(formatSessionTitle("first line\nsecond line\r\nthird"), "first line second line third"); @@ -9,3 +10,108 @@ test("formatSessionTitle replaces newlines with spaces", () => { test("formatSessionTitle truncates after normalizing whitespace", () => { assert.equal(formatSessionTitle("one\n two three", 10), "one two th…"); }); + +test("formatSessionStatus maps status values to display labels", () => { + assert.equal(formatSessionStatus("completed"), "done"); + assert.equal(formatSessionStatus("processing"), "running"); + assert.equal(formatSessionStatus("pending"), "pending"); + assert.equal(formatSessionStatus("waiting_for_user"), "waiting"); + assert.equal(formatSessionStatus("failed"), "failed"); + assert.equal(formatSessionStatus("interrupted"), "stopped"); + assert.equal(formatSessionStatus("unknown_status" as any), "unknown_status"); +}); + +test("filterSessions returns all sessions when query is empty", () => { + const sessions = buildSessions([{ summary: "Fix login bug" }, { summary: "Add dark mode" }]); + assert.equal(filterSessions(sessions, "").length, 2); + assert.equal(filterSessions(sessions, " ").length, 2); +}); + +test("filterSessions matches by summary (case-insensitive)", () => { + const sessions = buildSessions([ + { summary: "Fix login bug" }, + { summary: "Add dark mode" }, + { summary: "Refactor auth module" }, + ]); + + assert.equal(filterSessions(sessions, "login").length, 1); + assert.equal(filterSessions(sessions, "LOGIN").length, 1); + assert.equal(filterSessions(sessions, "Login").length, 1); +}); + +test("filterSessions matches by status (case-insensitive)", () => { + const sessions = buildSessions([ + { summary: "Task 1", status: "completed" }, + { summary: "Task 2", status: "failed" }, + { summary: "Task 3", status: "completed" }, + ]); + + assert.equal(filterSessions(sessions, "failed").length, 1); + assert.equal(filterSessions(sessions, "completed").length, 2); +}); + +test("filterSessions matches by failReason", () => { + const sessions = buildSessions([ + { summary: "Task 1", status: "failed", failReason: "API key not found" }, + { summary: "Task 2", status: "completed" }, + ]); + + assert.equal(filterSessions(sessions, "API key").length, 1); + assert.equal(filterSessions(sessions, "not found").length, 1); +}); + +test("filterSessions matches by assistantReply", () => { + const sessions = buildSessions([ + { summary: "Task 1", assistantReply: "The bug was fixed by updating the config." }, + { summary: "Task 2", assistantReply: "Dark mode has been added successfully." }, + ]); + + assert.equal(filterSessions(sessions, "dark mode").length, 1); + assert.equal(filterSessions(sessions, "config").length, 1); +}); + +test("filterSessions returns empty array when no match", () => { + const sessions = buildSessions([{ summary: "Fix login bug" }, { summary: "Add dark mode" }]); + + assert.equal(filterSessions(sessions, "nonexistent").length, 0); +}); + +test("filterSessions matches across multiple fields on same session", () => { + const sessions = buildSessions([ + { summary: "Fix login bug", status: "failed", failReason: "Timeout error" }, + { summary: "Add dark mode", status: "completed" }, + ]); + + // Should match the first session via status + assert.equal(filterSessions(sessions, "failed").length, 1); + // Should match the first session via failReason + assert.equal(filterSessions(sessions, "timeout").length, 1); + // Partial summary match + assert.equal(filterSessions(sessions, "login").length, 1); +}); + +test("filterSessions handles sessions with null fields", () => { + const sessions = buildSessions([{ summary: null }, { summary: "Valid summary" }]); + + assert.equal(filterSessions(sessions, "valid").length, 1); + assert.equal(filterSessions(sessions, "summary").length, 1); +}); + +function buildSessions(overrides: Array>): SessionEntry[] { + return overrides.map((override, i) => ({ + id: `session-${i}`, + summary: override.summary ?? null, + assistantReply: override.assistantReply ?? null, + assistantThinking: null, + assistantRefusal: null, + toolCalls: null, + status: override.status ?? "completed", + failReason: override.failReason ?? null, + usage: null, + usagePerModel: null, + activeTokens: 0, + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + processes: null, + })); +} diff --git a/src/ui/SessionList.tsx b/src/ui/SessionList.tsx index fdbd1fee..ab3bf751 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/SessionList.tsx @@ -1,6 +1,6 @@ -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { SessionEntry } from "../session"; +import type { SessionEntry, SessionStatus } from "../session"; type Props = { sessions: SessionEntry[]; @@ -8,25 +8,57 @@ type Props = { onCancel: () => void; }; +/** + * Filter sessions by a search query. + * Matches against summary, status, and failReason fields (case-insensitive). + * Returns all sessions when query is empty. + */ +export function filterSessions(sessions: SessionEntry[], query: string): SessionEntry[] { + if (!query.trim()) { + return sessions; + } + + const lowerQuery = query.toLowerCase().trim(); + return sessions.filter((session) => { + if (session.summary && session.summary.toLowerCase().includes(lowerQuery)) { + return true; + } + if (session.status.toLowerCase().includes(lowerQuery)) { + return true; + } + if (session.failReason && session.failReason.toLowerCase().includes(lowerQuery)) { + return true; + } + if (session.assistantReply && session.assistantReply.toLowerCase().includes(lowerQuery)) { + return true; + } + return false; + }); +} + export function SessionList({ sessions, onSelect, onCancel }: Props): React.ReactElement { const [index, setIndex] = useState(0); + const [searchQuery, setSearchQuery] = useState(""); const { columns, rows } = useWindowSize(); + // Filter sessions by search query + const filteredSessions = useMemo(() => filterSessions(sessions, searchQuery), [sessions, searchQuery]); + + // Reset index when filtered list changes (e.g., query changes) + const safeIndex = useMemo(() => { + if (filteredSessions.length === 0) return 0; + return Math.max(0, Math.min(index, filteredSessions.length - 1)); + }, [index, filteredSessions.length]); + // Dynamically calculate the number of visible sessions based on terminal height const maxVisibleSessions = useMemo(() => { - // Subtract space used by borders, header, footer, scroll indicator, etc. - // Outer container height=rows-1, outer border 2 + header 1 + inner border 2 + footer 1 + scroll indicator 1 = 8 - const reservedLines = 8; + // Subtract space used by borders, header (2 lines with search bar), footer, scroll indicator, etc. + // Outer container height=rows-1, outer border 2 + header 2 + search bar 1 + inner border 2 + footer 1 + scroll indicator 1 = 9 + const reservedLines = searchQuery ? 12 : 9; const linesPerSession = 3; // height=2 + marginBottom=1 const availableLines = Math.max(0, Math.min(rows, 30) - reservedLines); return Math.max(1, Math.floor(availableLines / linesPerSession)); - }, [rows]); - - // Ensure index stays within valid range - const safeIndex = useMemo(() => { - if (sessions.length === 0) return 0; - return Math.max(0, Math.min(index, sessions.length - 1)); - }, [index, sessions.length]); + }, [rows, searchQuery]); // Calculate scroll offset to keep the selected item visible const scrollOffset = useMemo(() => { @@ -36,23 +68,63 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac // Get the currently visible session list const visibleSessions = useMemo(() => { - return sessions.slice(scrollOffset, scrollOffset + maxVisibleSessions); - }, [sessions, scrollOffset, maxVisibleSessions]); + return filteredSessions.slice(scrollOffset, scrollOffset + maxVisibleSessions); + }, [filteredSessions, scrollOffset, maxVisibleSessions]); + + // Handle backspace for search query + const handleBackspace = useCallback(() => { + setSearchQuery((prev) => prev.slice(0, -1)); + setIndex(0); + }, []); useInput((input, key) => { - if (key.escape || (key.ctrl && (input === "c" || input === "C"))) { + // ESC: clear search first, then cancel + if (key.escape) { + if (searchQuery) { + setSearchQuery(""); + setIndex(0); + return; + } onCancel(); return; } - if (sessions.length === 0) { + + // Ctrl+C also cancels + if (key.ctrl && (input === "c" || input === "C")) { + onCancel(); return; } + + // Backspace / Delete: remove last search character + if (key.backspace || key.delete) { + if (searchQuery) { + handleBackspace(); + return; + } + // If no search query, navigation keys below handle the rest + } + + // Printable character: append to search query + if (input && input.length > 0 && !key.meta && !key.ctrl && !key.tab) { + // Ignore if it's a named key that happens to have input (safety check) + if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) { + return; + } + setSearchQuery((prev) => prev + input); + setIndex(0); + return; + } + + if (filteredSessions.length === 0) { + return; + } + if (key.upArrow) { setIndex((i) => Math.max(0, i - 1)); return; } if (key.downArrow) { - setIndex((i) => Math.min(sessions.length - 1, i + 1)); + setIndex((i) => Math.min(filteredSessions.length - 1, i + 1)); return; } if (key.pageUp) { @@ -60,7 +132,7 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac return; } if (key.pageDown) { - setIndex((i) => Math.min(sessions.length - 1, i + maxVisibleSessions)); + setIndex((i) => Math.min(filteredSessions.length - 1, i + maxVisibleSessions)); return; } if (key.home) { @@ -68,17 +140,19 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac return; } if (key.end) { - setIndex(sessions.length - 1); + setIndex(filteredSessions.length - 1); return; } if (key.return) { - const session = sessions[safeIndex]; + const session = filteredSessions[safeIndex]; if (session) { onSelect(session.id); } } }); + const hasActiveSearch = searchQuery.trim().length > 0; + if (sessions.length === 0) { return ( @@ -99,15 +173,24 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac > {/* Header row */} - - - Resume a session - - - {" "} - ({sessions.length} total) - + + + + Resume a session + + + {" "} + ({sessions.length} total + {hasActiveSearch ? `, ${filteredSessions.length} matched` : ""}) + + + {/* Search bar */} + + {searchQuery ? `Search: ${searchQuery}` : "Type to search\u2026"} + {searchQuery ? | : null} + + {/* Session list */} - {visibleSessions.map((session, i) => { - const actualIndex = scrollOffset + i; - return ( - - - {actualIndex === safeIndex ? "> " : " "} - - - - - {formatSessionTitle(session.summary || "Untitled")} - - ({session.status}) + {filteredSessions.length === 0 ? ( + + No sessions match "{searchQuery}". + + ) : ( + visibleSessions.map((session, i) => { + const actualIndex = scrollOffset + i; + return ( + + + {actualIndex === safeIndex ? "> " : " "} - - {formatTimestamp(session.updateTime)} + + + + {formatSessionTitle(session.summary || "Untitled")} + + ({formatSessionStatus(session.status)}) + + + {formatTimestamp(session.updateTime)} + - - ); - })} - {scrollOffset > 0 || scrollOffset + maxVisibleSessions < sessions.length ? ( + ); + }) + )} + {scrollOffset > 0 || scrollOffset + maxVisibleSessions < filteredSessions.length ? ( - {scrollOffset > 0 ? … {scrollOffset} newer sessions above. : null} - {scrollOffset + maxVisibleSessions < sessions.length ? ( - … {sessions.length - scrollOffset - maxVisibleSessions} older sessions below. + {scrollOffset > 0 ? … {scrollOffset} sessions above. : null} + {scrollOffset + maxVisibleSessions < filteredSessions.length ? ( + … {filteredSessions.length - scrollOffset - maxVisibleSessions} sessions below. ) : null} ) : null} {/* Footer */} - - ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel + + {hasActiveSearch ? ( + + Esc clear search · + ↑/↓ navigate · Enter select · Esc again to cancel + + ) : ( + + Type to search · ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel + + )} @@ -179,6 +277,25 @@ export function formatSessionTitle(value: string, max = 70): string { return truncate(value.replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim(), max); } +export function formatSessionStatus(status: SessionStatus): string { + switch (status) { + case "completed": + return "done"; + case "processing": + return "running"; + case "pending": + return "pending"; + case "waiting_for_user": + return "waiting"; + case "failed": + return "failed"; + case "interrupted": + return "stopped"; + default: + return status; + } +} + function truncate(value: string, max: number): string { if (value.length <= max) { return value; diff --git a/src/ui/index.ts b/src/ui/index.ts index efb4edd5..f639c65c 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -37,7 +37,7 @@ export { } from "./PromptInput"; export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./prompt/cursor"; -export { SessionList, formatSessionTitle } from "./SessionList"; +export { SessionList, formatSessionTitle, filterSessions, formatSessionStatus } from "./SessionList"; export { ThemedGradient } from "./ThemedGradient"; export { UpdatePrompt, type UpdatePromptChoice } from "./UpdatePrompt"; export { WelcomeScreen, formatHomeRelativePath, buildWelcomeTips } from "./WelcomeScreen"; From 208a9886196c404dd00c052a4bcc45e4a1ed4a0c Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 14:38:00 +0800 Subject: [PATCH 038/212] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8D=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=9F=A5=E8=AF=A2=E4=B8=AD=E5=9B=9E=E8=BD=A6=E9=94=AE?= =?UTF-8?q?=E7=9A=84=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在处理搜索输入时忽略回车键,避免错误触发搜索操作 - 增加对回车键的判断,提升输入处理的准确性 - 防止命名键在输入查询时误触发逻辑分支 --- src/ui/SessionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/SessionList.tsx b/src/ui/SessionList.tsx index ab3bf751..5f186bd9 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/SessionList.tsx @@ -105,7 +105,7 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac } // Printable character: append to search query - if (input && input.length > 0 && !key.meta && !key.ctrl && !key.tab) { + if (input && input.length > 0 && !key.meta && !key.ctrl && !key.tab && !key.return) { // Ignore if it's a named key that happens to have input (safety check) if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) { return; From 585fe5ff442b0dcbfeb6a68a959c68badb386fcd Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 14:48:05 +0800 Subject: [PATCH 039/212] merge(branch): 'main' into refactor/extract-dropdown-components --- src/ui/PromptInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 24aa0767..7008cba9 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -238,7 +238,6 @@ export const PromptInput = React.memo(function PromptInput({ setSelectedSkills([]); setShowSkillsDropdown(false); setOpenRawModelDropdown(false); - setModelDropdownStep(null); setHistoryCursor(-1); setDraftBeforeHistory(null); clearPromptUndoRedoState(undoRedoRef.current); From 3c15003454ce743a2577fbaa278579c88e675fb6 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 20 May 2026 14:53:10 +0800 Subject: [PATCH 040/212] refactor: extracted terminal data dispatch into `dispatchTerminalInput()` --- src/tests/promptInputKeys.test.ts | 51 +++++++++++++++++++++++++++ src/ui/PromptInput.tsx | 2 +- src/ui/index.ts | 1 + src/ui/prompt/index.ts | 2 +- src/ui/prompt/useTerminalInput.ts | 58 +++++++++++++++++-------------- 5 files changed, 86 insertions(+), 28 deletions(-) diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts index 54213a12..4f8b4d95 100644 --- a/src/tests/promptInputKeys.test.ts +++ b/src/tests/promptInputKeys.test.ts @@ -20,11 +20,23 @@ import { renderBufferWithCursor, buildInitPromptSubmission, buildPromptDraftFromSessionMessage, + dispatchTerminalInput, disableTerminalExtendedKeys, enableTerminalExtendedKeys, + EMPTY_BUFFER, + insertText, + backspace, } from "../ui"; import type { SessionMessage, SkillInfo } from "../session"; +function collectDispatchedInput(data: string) { + const events: ReturnType[] = []; + dispatchTerminalInput(data, (input, key) => { + events.push({ input, key }); + }); + return events; +} + test("parseTerminalInput treats DEL bytes as backspace", () => { const { input, key } = parseTerminalInput("\u007F"); assert.equal(input, ""); @@ -72,6 +84,45 @@ test("parseTerminalInput keeps DEL payload for meta+backspace", () => { assert.equal(key.backspace, false); }); +test("dispatchTerminalInput splits iOS CJK composition packets", () => { + const events = collectDispatchedInput("가\u007F나"); + assert.equal(events.length, 3); + assert.equal(events[0]?.input, "가"); + assert.equal(events[1]?.input, ""); + assert.equal(events[1]?.key.backspace, true); + assert.equal(events[2]?.input, "나"); +}); + +test("dispatchTerminalInput applies multi-step CJK composition to the prompt buffer", () => { + let state = EMPTY_BUFFER; + dispatchTerminalInput("ㄱ\u007F가\u007F각", (input, key) => { + if (key.backspace) { + state = backspace(state); + return; + } + state = insertText(state, input); + }); + + assert.equal(state.text, "각"); + assert.equal(state.cursor, 1); +}); + +test("dispatchTerminalInput preserves meta+backspace as one event", () => { + const events = collectDispatchedInput("\u001B\u007F"); + assert.equal(events.length, 1); + assert.equal(events[0]?.input, "\u007F"); + assert.equal(events[0]?.key.meta, true); + assert.equal(events[0]?.key.backspace, false); + assert.equal(events[0]?.key.escape, false); +}); + +test("dispatchTerminalInput emits consecutive backspaces from one packet", () => { + const events = collectDispatchedInput("\u007F\u007F"); + assert.equal(events.length, 2); + assert.equal(events[0]?.key.backspace, true); + assert.equal(events[1]?.key.backspace, true); +}); + test("parseTerminalInput keeps BS payload for meta+backspace", () => { const { input, key } = parseTerminalInput("\u001B\b"); assert.equal(input, "\b"); diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 6767560f..3cb51f28 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -42,7 +42,7 @@ import { readClipboardImageAsync } from "./clipboard"; import type { SessionEntry, SkillInfo } from "../session"; // Re-exported from prompt modules for backward compatibility -export { useTerminalInput, parseTerminalInput } from "./prompt"; +export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./prompt"; export type { InputKey } from "./prompt"; import { useTerminalInput } from "./prompt"; diff --git a/src/ui/index.ts b/src/ui/index.ts index 7c0ed153..681d77cf 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -30,6 +30,7 @@ export { MODEL_COMMAND_THINKING_OPTIONS, useTerminalInput, parseTerminalInput, + dispatchTerminalInput, type PromptSubmission, type PromptDraft, type InputKey, diff --git a/src/ui/prompt/index.ts b/src/ui/prompt/index.ts index a33172c7..59075581 100644 --- a/src/ui/prompt/index.ts +++ b/src/ui/prompt/index.ts @@ -1,4 +1,4 @@ -export { useTerminalInput, parseTerminalInput } from "./useTerminalInput"; +export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./useTerminalInput"; export type { InputKey } from "./useTerminalInput"; export { diff --git a/src/ui/prompt/useTerminalInput.ts b/src/ui/prompt/useTerminalInput.ts index 8fe0d60b..9ce69766 100644 --- a/src/ui/prompt/useTerminalInput.ts +++ b/src/ui/prompt/useTerminalInput.ts @@ -169,6 +169,37 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key: return { input, key }; } +export function dispatchTerminalInput( + data: Buffer | string, + inputHandler: (input: string, key: InputKey) => void +): void { + const raw = String(data); + + // Fix CJK composition bug on iOS terminals (Moshi, Blink, etc.). + // iOS keyboards can send composed characters as a single packet like: + // "가\x7f나" (character + backspace + replacement character) + // Do not split escape-prefixed sequences such as Alt+Backspace. + if (!raw.startsWith("\u001B") && raw.includes("\x7f") && raw.length > 1) { + const parts = raw.split("\x7f"); + if (parts[0]) { + const { input, key } = parseTerminalInput(parts[0]); + inputHandler(input, key); + } + for (let i = 1; i < parts.length; i++) { + const bs = parseTerminalInput("\x7f"); + inputHandler(bs.input, bs.key); + if (parts[i]) { + const { input, key } = parseTerminalInput(parts[i]); + inputHandler(input, key); + } + } + return; + } + + const { input, key } = parseTerminalInput(data); + inputHandler(input, key); +} + export function useTerminalInput( inputHandler: (input: string, key: InputKey) => void, options: { isActive?: boolean } = {} @@ -193,32 +224,7 @@ export function useTerminalInput( return; } const handleData = (data: Buffer | string) => { - const raw = String(data); - - // Fix CJK composition bug on iOS terminals (Moshi, Blink, etc.). - // iOS keyboards send composed characters as a single packet like: - // "가\x7f나" (character + backspace + new character) - // Without splitting, parseTerminalInput treats the whole packet as - // one input and drops the composition backspaces, corrupting the text. - if (raw.includes("\x7f") && raw.length > 1) { - const parts = raw.split("\x7f"); - if (parts[0]) { - const { input, key } = parseTerminalInput(parts[0]); - handlerRef.current(input, key); - } - for (let i = 1; i < parts.length; i++) { - const bs = parseTerminalInput("\x7f"); - handlerRef.current(bs.input, bs.key); - if (parts[i]) { - const { input, key } = parseTerminalInput(parts[i]); - handlerRef.current(input, key); - } - } - return; - } - - const { input, key } = parseTerminalInput(data); - handlerRef.current(input, key); + dispatchTerminalInput(data, handlerRef.current); }; stdin?.on("data", handleData); From abde38cc76be4cd1836d04713d2f51cca294d11b Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 20 May 2026 15:40:35 +0800 Subject: [PATCH 041/212] feat: refresh cached MCP tool definitions after server crash --- src/mcp/mcp-manager.ts | 1 + src/tests/session.test.ts | 64 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/mcp/mcp-manager.ts b/src/mcp/mcp-manager.ts index 217e3fc1..fe8066b4 100644 --- a/src/mcp/mcp-manager.ts +++ b/src/mcp/mcp-manager.ts @@ -236,6 +236,7 @@ export class McpManager { this.tools = this.tools.filter((t) => t.serverName !== name); this.prompts = this.prompts.filter((p) => p.serverName !== name); this.resources = this.resources.filter((r) => r.serverName !== name); + this.onToolsListChanged?.(); this.setStatus({ name, status: "failed", diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 27b504e6..9f3c7fb5 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -482,6 +482,60 @@ rl.on("line", (line) => { assert.deepEqual(manager.getMcpStatus(), []); }); +test("SessionManager refreshes cached MCP tool definitions after server crash", async () => { + const workspace = createTempDir("deepcode-mcp-crash-cache-workspace-"); + const serverPath = path.join(workspace, "mcp-server-crash.cjs"); + fs.writeFileSync( + serverPath, + ` +const readline = require("readline"); +const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); +function send(message) { + process.stdout.write(JSON.stringify(message) + "\\n"); +} +rl.on("line", (line) => { + const request = JSON.parse(line); + if (!("id" in request)) { + return; + } + if (request.method === "initialize") { + send({ jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} } } }); + return; + } + if (request.method === "tools/list") { + send({ jsonrpc: "2.0", id: request.id, result: { tools: [ + { name: "echo", inputSchema: { type: "object", properties: {} } } + ] } }); + return; + } + if (request.method === "prompts/list") { + send({ jsonrpc: "2.0", id: request.id, result: { prompts: [] } }); + return; + } + if (request.method === "resources/list") { + send({ jsonrpc: "2.0", id: request.id, result: { resources: [] } }); + setTimeout(() => process.exit(9), 10); + return; + } + send({ jsonrpc: "2.0", id: request.id, result: { content: [] } }); +}); +`, + "utf8" + ); + + const manager = createSessionManager(workspace, "machine-id-mcp-crash-cache"); + await manager.initMcpServers({ crashy: { command: process.execPath, args: [serverPath] } }); + + assert.equal(manager.getMcpStatus()[0]?.status, "ready"); + assert.equal((manager as any).mcpToolDefinitions.length, 1); + + await waitForMcpStatus(manager, "failed"); + + assert.equal((manager as any).mcpToolDefinitions.length, 0); + + manager.dispose(); +}); + test("SessionManager reports configured MCP servers as starting before initialization", () => { const workspace = createTempDir("deepcode-mcp-configured-workspace-"); const manager = new SessionManager({ @@ -2276,6 +2330,16 @@ async function waitForNotifyRecords( assert.fail(`expected ${expectedCount} notify records in ${outputPath}`); } +async function waitForMcpStatus(manager: SessionManager, expectedStatus: string): Promise { + for (let attempt = 0; attempt < 100; attempt += 1) { + if (manager.getMcpStatus()[0]?.status === expectedStatus) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 20)); + } + assert.fail(`expected MCP status ${expectedStatus}`); +} + function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } From b757ea191993207f2fe7761bf58730e111eec01a Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 16:51:07 +0800 Subject: [PATCH 042/212] =?UTF-8?q?refactor(ui):=20=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E5=B9=B6=E5=A4=8D=E7=94=A8=20isSkillSelected=20=E5=87=BD?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 isSkillSelected 函数从 PromptInput.tsx 移动到 SlashCommandMenu.tsx - 在 PromptInput.tsx 中从 SlashCommandMenu 导入 isSkillSelected - 简化代码结构,避免函数重复定义 - 统一技能选择判断逻辑以提高代码复用性 --- src/ui/PromptInput.tsx | 6 +----- src/ui/SlashCommandMenu.tsx | 5 ++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 5e2c2b51..074cab6f 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -48,7 +48,7 @@ export type { InputKey } from "./prompt"; import { useTerminalInput } from "./prompt"; import type { InputKey } from "./prompt"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, useTerminalFocusReporting } from "./prompt"; -import SlashCommandMenu from "./SlashCommandMenu"; +import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; import type { ModelConfigSelection } from "../settings"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; @@ -818,10 +818,6 @@ export function formatSelectedSkillsStatus(skills: SkillInfo[]): string { return `⚡ ${names.join(", ")}`; } -export function isSkillSelected(skills: SkillInfo[], skill: SkillInfo): boolean { - return skills.some((item) => item.name === skill.name); -} - export function addUniqueSkill(skills: SkillInfo[], skill: SkillInfo): SkillInfo[] { if (isSkillSelected(skills, skill)) { return skills; diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/SlashCommandMenu.tsx index 02ff3084..df599b54 100644 --- a/src/ui/SlashCommandMenu.tsx +++ b/src/ui/SlashCommandMenu.tsx @@ -3,6 +3,7 @@ import type { SlashCommandItem } from "./slashCommands"; import { ARGS_SEPARATOR } from "./constants"; import React from "react"; import { Box, Text } from "ink"; +import type { SkillInfo } from "../session"; type SlashCommandMenuProps = { items: SlashCommandItem[]; @@ -10,7 +11,9 @@ type SlashCommandMenuProps = { width: number; maxVisible?: number; }; - +export function isSkillSelected(skills: SkillInfo[], skill: SkillInfo): boolean { + return skills.some((item) => item.name === skill.name); +} const SlashCommandMenu = React.memo(function SlashCommandMenu({ items, activeIndex, From e0bde604fb220b63b8b43b17a72cf3b5ea0934fd Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 17:00:45 +0800 Subject: [PATCH 043/212] =?UTF-8?q?refactor(ui):=20=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E5=B9=B6=E5=A4=8D=E7=94=A8=20isSkillSelected=20=E5=87=BD?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 isSkillSelected 函数从 PromptInput.tsx 移动到 SlashCommandMenu.tsx - 在 PromptInput.tsx 中从 SlashCommandMenu 导入 isSkillSelected - 简化代码结构,避免函数重复定义 - 统一技能选择判断逻辑以提高代码复用性 --- src/ui/components/SkillsDropdown/index.tsx | 2 +- src/ui/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index 545e2abd..b320d249 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,8 +1,8 @@ import DropdownMenu from "../../DropdownMenu"; import React, { useEffect, useState } from "react"; -import { isSkillSelected } from "../../PromptInput"; import type { SkillInfo } from "../../../session"; import { useInput } from "ink"; +import { isSkillSelected } from "../../SlashCommandMenu"; const SkillsDropdown: React.FC<{ open: boolean; diff --git a/src/ui/index.ts b/src/ui/index.ts index d634ed6c..26e7eaac 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -23,7 +23,6 @@ export { IMAGE_ATTACHMENT_CLEAR_HINT, formatImageAttachmentStatus, formatSelectedSkillsStatus, - isSkillSelected, addUniqueSkill, toggleSkillSelection, removeCurrentSlashToken, From b2544b831252c5d58f15cbfa6b5c7c04e1a1aa8f Mon Sep 17 00:00:00 2001 From: lellansin Date: Wed, 20 May 2026 17:36:41 +0800 Subject: [PATCH 044/212] perf: reuse OpenAI client and add undici keep-alive Agent with connection warmup Extract OpenAI client creation logic into src/common/openai-client.ts: - Custom undici Agent with 60s keepAlive timeout (default is 4s) - Module-level client instance cache (reuse across calls) - Fire-and-forget connection warmup on first creation (3s timeout) - getMachineId() helper The App.tsx now simply imports and re-exports createOpenAIClient from the new common module, keeping UI concerns separate from HTTP/client lifecycle management. --- package-lock.json | 10 +++ package.json | 1 + src/common/openai-client.ts | 117 ++++++++++++++++++++++++++++++++++++ src/ui/App.tsx | 73 +++------------------- src/ui/index.ts | 2 +- 5 files changed, 138 insertions(+), 65 deletions(-) create mode 100644 src/common/openai-client.ts diff --git a/package-lock.json b/package-lock.json index 17a77cae..cdb85dea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "ink-gradient": "^4.0.0", "openai": "^6.35.0", "react": "^19.2.5", + "undici": "^7.25.0", "zod": "^4.4.3" }, "bin": { @@ -4096,6 +4097,15 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", diff --git a/package.json b/package.json index b72fd96a..bf8d167e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "ink-gradient": "^4.0.0", "openai": "^6.35.0", "react": "^19.2.5", + "undici": "^7.25.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/src/common/openai-client.ts b/src/common/openai-client.ts new file mode 100644 index 00000000..7f9634c3 --- /dev/null +++ b/src/common/openai-client.ts @@ -0,0 +1,117 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import OpenAI from "openai"; +import { Agent, fetch as undiciFetch } from "undici"; +import { resolveCurrentSettings } from "../ui/App"; + +// Custom undici Agent with a 60-second keepAlive timeout. The default +// global fetch (undici) only keeps connections alive for 4 seconds, which +// is too short for a CLI where the user may spend 10–30 seconds reading +// output between prompts. By passing a dedicated Agent to undiciFetch we +// keep connections reusable for a full minute after the last request. +const keepAliveAgent = new Agent({ keepAliveTimeout: 60_000 }); + +// Module-level cache for the OpenAI client instance. The client itself is +// a stateless fetch wrapper, so it is safe to share across calls as long as +// the apiKey + baseURL stay the same. Model, thinking-mode and other +// settings are always read fresh from the project / user config files. +let cachedOpenAI: OpenAI | null = null; +let cachedOpenAIKey = ""; + +export function createOpenAIClient(projectRoot: string = process.cwd()): { + client: OpenAI | null; + model: string; + baseURL: string; + thinkingEnabled: boolean; + reasoningEffort: "high" | "max"; + debugLogEnabled: boolean; + notify?: string; + webSearchTool?: string; + env: Record; + machineId?: string; +} { + const settings = resolveCurrentSettings(projectRoot); + if (!settings.apiKey) { + return { + client: null, + model: settings.model, + baseURL: settings.baseURL, + thinkingEnabled: settings.thinkingEnabled, + reasoningEffort: settings.reasoningEffort, + debugLogEnabled: settings.debugLogEnabled, + notify: settings.notify, + webSearchTool: settings.webSearchTool, + env: settings.env, + machineId: getMachineId(), + }; + } + + const cacheKey = `${settings.apiKey}::${settings.baseURL}`; + if (cachedOpenAI && cachedOpenAIKey === cacheKey) { + return { + client: cachedOpenAI, + model: settings.model, + baseURL: settings.baseURL, + thinkingEnabled: settings.thinkingEnabled, + reasoningEffort: settings.reasoningEffort, + debugLogEnabled: settings.debugLogEnabled, + notify: settings.notify, + webSearchTool: settings.webSearchTool, + env: settings.env, + machineId: getMachineId(), + }; + } + + cachedOpenAI = new OpenAI({ + apiKey: settings.apiKey, + baseURL: settings.baseURL || undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: keepAliveAgent }), + }); + cachedOpenAIKey = cacheKey; + + // Fire-and-forget warmup: pre-establish TCP+TLS connection to the API + // server while the user is composing their first prompt. Bounded by a + // short timeout so a slow / unreachable API never blocks process exit. + void (async () => { + const ac = new AbortController(); + const timer = setTimeout(() => ac.abort(), 3000); + try { + await cachedOpenAI.models.list({ signal: ac.signal }).catch(() => {}); + } finally { + clearTimeout(timer); + } + })(); + + return { + client: cachedOpenAI, + model: settings.model, + baseURL: settings.baseURL, + thinkingEnabled: settings.thinkingEnabled, + reasoningEffort: settings.reasoningEffort, + debugLogEnabled: settings.debugLogEnabled, + notify: settings.notify, + webSearchTool: settings.webSearchTool, + env: settings.env, + machineId: getMachineId(), + }; +} + +function getMachineId(): string | undefined { + try { + const idPath = path.join(os.homedir(), ".deepcode", "machine-id"); + if (fs.existsSync(idPath)) { + const raw = fs.readFileSync(idPath, "utf8").trim(); + if (raw) { + return raw; + } + } + const generated = `${os.hostname()}-${Math.random().toString(36).slice(2)}-${Date.now()}`; + fs.mkdirSync(path.dirname(idPath), { recursive: true }); + fs.writeFileSync(idPath, generated, "utf8"); + return generated; + } catch { + return undefined; + } +} diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 75d66899..5419a2ad 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -4,7 +4,7 @@ import chalk from "chalk"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import OpenAI from "openai"; +import { createOpenAIClient } from "../common/openai-client"; import { type LlmStreamProgress, type MessageMeta, @@ -166,6 +166,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. void refreshSkills(); }, [refreshSessionsList, refreshSkills]); + // Eagerly create the OpenAI client on mount so the TCP+TLS connection + // warmup (fire-and-forget inside createOpenAIClient) starts before the + // user sends their first prompt. + useEffect(() => { + createOpenAIClient(projectRoot); + }, [projectRoot]); + useLayoutEffect(() => { const settings = resolveCurrentSettings(projectRoot); void sessionManager.initMcpServers(settings.mcpServers); @@ -838,69 +845,7 @@ export function resolveCurrentSettings(projectRoot: string = process.cwd()): Res ); } -export function createOpenAIClient(projectRoot: string = process.cwd()): { - client: OpenAI | null; - model: string; - baseURL: string; - thinkingEnabled: boolean; - reasoningEffort: "high" | "max"; - debugLogEnabled: boolean; - notify?: string; - webSearchTool?: string; - env: Record; - machineId?: string; -} { - const settings = resolveCurrentSettings(projectRoot); - if (!settings.apiKey) { - return { - client: null, - model: settings.model, - baseURL: settings.baseURL, - thinkingEnabled: settings.thinkingEnabled, - reasoningEffort: settings.reasoningEffort, - debugLogEnabled: settings.debugLogEnabled, - notify: settings.notify, - webSearchTool: settings.webSearchTool, - env: settings.env, - machineId: getMachineId(), - }; - } - - const client = new OpenAI({ - apiKey: settings.apiKey, - baseURL: settings.baseURL || undefined, - }); - return { - client, - model: settings.model, - baseURL: settings.baseURL, - thinkingEnabled: settings.thinkingEnabled, - reasoningEffort: settings.reasoningEffort, - debugLogEnabled: settings.debugLogEnabled, - notify: settings.notify, - webSearchTool: settings.webSearchTool, - env: settings.env, - machineId: getMachineId(), - }; -} - -function getMachineId(): string | undefined { - try { - const idPath = path.join(os.homedir(), ".deepcode", "machine-id"); - if (fs.existsSync(idPath)) { - const raw = fs.readFileSync(idPath, "utf8").trim(); - if (raw) { - return raw; - } - } - const generated = `${os.hostname()}-${Math.random().toString(36).slice(2)}-${Date.now()}`; - fs.mkdirSync(path.dirname(idPath), { recursive: true }); - fs.writeFileSync(idPath, generated, "utf8"); - return generated; - } catch { - return undefined; - } -} +export { createOpenAIClient } from "../common/openai-client"; function getUserSettingsPath(): string { return path.join(os.homedir(), ".deepcode", "settings.json"); diff --git a/src/ui/index.ts b/src/ui/index.ts index 26e7eaac..d899d4b4 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -11,9 +11,9 @@ export { writeProjectSettings, writeModelConfigSelection, resolveCurrentSettings, - createOpenAIClient, buildPromptDraftFromSessionMessage, } from "./App"; +export { createOpenAIClient } from "../common/openai-client"; export { default as AppContainer } from "./AppContainer"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; export { MessageView } from "./components"; From 7578c324639fccdccee32870b193111aaeff3183 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 20 May 2026 18:12:54 +0800 Subject: [PATCH 045/212] feat: add built-in tool alias mapping --- src/tests/tool-executor.test.ts | 41 +++++++++++++++++++++++++++++++++ src/tools/executor.ts | 10 +++++++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/tests/tool-executor.test.ts diff --git a/src/tests/tool-executor.test.ts b/src/tests/tool-executor.test.ts new file mode 100644 index 00000000..f7def2fe --- /dev/null +++ b/src/tests/tool-executor.test.ts @@ -0,0 +1,41 @@ +import { afterEach, test } from "node:test"; +import assert from "node:assert/strict"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { ToolExecutor } from "../tools/executor"; + +const tempDirs: string[] = []; + +afterEach(() => { + while (tempDirs.length > 0) { + const dir = tempDirs.pop(); + if (dir) { + fs.rmSync(dir, { recursive: true, force: true }); + } + } +}); + +test("ToolExecutor accepts title-case built-in tool aliases", async () => { + const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-tool-executor-")); + tempDirs.push(workspace); + const filePath = path.join(workspace, "sample.txt"); + fs.writeFileSync(filePath, "alpha\nbeta\n", "utf8"); + + const executor = new ToolExecutor(workspace); + const executions = await executor.executeToolCalls("alias-session", [ + { + id: "call-read", + type: "function", + function: { + name: "Read", + arguments: JSON.stringify({ file_path: filePath }) + } + } + ]); + + assert.equal(executions.length, 1); + assert.equal(executions[0]?.result.ok, true); + assert.equal(executions[0]?.result.name, "read"); + assert.match(executions[0]?.result.output ?? "", /alpha/); +}); diff --git a/src/tools/executor.ts b/src/tools/executor.ts index 73e31f5e..edfca6fc 100644 --- a/src/tools/executor.ts +++ b/src/tools/executor.ts @@ -89,6 +89,13 @@ export type ToolHandler = ( context: ToolExecutionContext ) => Promise; +const BUILT_IN_TOOL_NAME_ALIASES = new Map([ + ["Bash", "bash"], + ["Read", "read"], + ["Write", "write"], + ["Edit", "edit"] +]); + export type ToolCallExecution = { toolCallId: string; content: string; @@ -187,7 +194,8 @@ export class ToolExecutor { hooks?: ToolExecutionHooks ): Promise { const toolName = toolCall.function.name; - const handler = this.toolHandlers.get(toolName); + const handlerName = BUILT_IN_TOOL_NAME_ALIASES.get(toolName) ?? toolName; + const handler = this.toolHandlers.get(handlerName); if (!handler) { // Try MCP tools if (this.mcpManager?.isMcpTool(toolName)) { From e424e187cfb1ecaa07f2673138464e303fa7e699 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 21:30:49 +0800 Subject: [PATCH 046/212] =?UTF-8?q?refactor(ui):=20=E4=BD=BF=E7=94=A8=20re?= =?UTF-8?q?setPromptInput=20=E7=AE=80=E5=8C=96=E6=92=A4=E9=94=80=E5=92=8C?= =?UTF-8?q?=E5=9B=9E=E7=BB=95=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换手动状态重置为调用 resetPromptInput 函数 - 简化代码提高可读性和维护性 - 保持撤销(undo)和回绕(rewind)命令处理逻辑一致 - 移除重复的状态重置代码块 --- src/ui/PromptInput.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 074cab6f..d2af5343 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -659,11 +659,7 @@ export const PromptInput = React.memo(function PromptInput({ } if (item.kind === "undo") { onSubmit({ text: "/undo", imageUrls: [], command: "undo" }); - setBuffer(EMPTY_BUFFER); - clearUndoRedoStacks(); - setImageUrls([]); - setSelectedSkills([]); - setShowSkillsDropdown(false); + resetPromptInput(); return; } if (item.kind === "mcp") { From a858684cf1d246d597552f0f16dc65bdedde422d Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 20 May 2026 21:36:50 +0800 Subject: [PATCH 047/212] =?UTF-8?q?fix(executor):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=E4=B8=8E=E8=AF=AD=E6=B3=95?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在工具类型列表的最后一项添加缺失的逗号 - 修正测试用例中 JSON 对象的格式错误 - 确保工具调用参数语法规范合理 - 修复断言前的代码缩进问题 --- src/tests/tool-executor.test.ts | 6 +++--- src/tools/executor.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/tool-executor.test.ts b/src/tests/tool-executor.test.ts index f7def2fe..f36def28 100644 --- a/src/tests/tool-executor.test.ts +++ b/src/tests/tool-executor.test.ts @@ -29,9 +29,9 @@ test("ToolExecutor accepts title-case built-in tool aliases", async () => { type: "function", function: { name: "Read", - arguments: JSON.stringify({ file_path: filePath }) - } - } + arguments: JSON.stringify({ file_path: filePath }), + }, + }, ]); assert.equal(executions.length, 1); diff --git a/src/tools/executor.ts b/src/tools/executor.ts index edfca6fc..220fc894 100644 --- a/src/tools/executor.ts +++ b/src/tools/executor.ts @@ -93,7 +93,7 @@ const BUILT_IN_TOOL_NAME_ALIASES = new Map([ ["Bash", "bash"], ["Read", "read"], ["Write", "write"], - ["Edit", "edit"] + ["Edit", "edit"], ]); export type ToolCallExecution = { From 3a8041faa173f09fac437318f15cfefb4d9c2f78 Mon Sep 17 00:00:00 2001 From: Kayro Date: Wed, 20 May 2026 21:30:27 +0800 Subject: [PATCH 048/212] feat(ui): add bracketed paste detection with large-paste marker collapsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detect bracketed paste (ESC[200~ / ESC[201~) and dispatch as atomic paste event - Large pastes (>10 lines or >1000 chars) are stored and replaced with a compact marker [paste #N] - Ctrl+O toggles expand/collapse, backspace/delete atomically remove the entire marker - Markers are highlighted with chalk.yellow and expanded back on submit - Follows existing terminal hook patterns (useBracketedPaste alongside useTerminalExtendedKeys) - Array-based chunk buffering to avoid O(n²) string concatenation on multi-chunk pastes - Lazy text cleaning deferred to expand/submit time Known limitation: expand/collapse briefly clears Ink content above the prompt (React render pipeline constraint). Reference: PR #45 (closed), inspired by pi project's paste marker approach. --- src/ui/PromptInput.tsx | 234 ++++++++++++++++++++++++++++-- src/ui/prompt/cursor.ts | 21 +++ src/ui/prompt/index.ts | 1 + src/ui/prompt/useTerminalInput.ts | 110 ++++++++++++++ src/ui/promptBuffer.ts | 114 +++++++++++++++ 5 files changed, 465 insertions(+), 15 deletions(-) diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 074cab6f..ad73d838 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -4,11 +4,18 @@ import chalk from "chalk"; import { ARGS_SEPARATOR } from "./constants"; import { EMPTY_BUFFER, + PASTE_MARKER_REGEX, backspace, + cleanPasteContent, deleteForward, + deletePasteMarkerBackward, + deletePasteMarkerForward, deleteWordBefore, deleteWordAfter, + expandPasteMarkers, + findPasteMarkerContaining, getCurrentSlashToken, + hasActivePasteMarkers, insertText, isEmpty, killLine, @@ -47,7 +54,12 @@ export type { InputKey } from "./prompt"; import { useTerminalInput } from "./prompt"; import type { InputKey } from "./prompt"; -import { useHiddenTerminalCursor, useTerminalExtendedKeys, useTerminalFocusReporting } from "./prompt"; +import { + useHiddenTerminalCursor, + useTerminalExtendedKeys, + useBracketedPaste, + useTerminalFocusReporting, +} from "./prompt"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; import type { ModelConfigSelection } from "../settings"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; @@ -143,6 +155,12 @@ export const PromptInput = React.memo(function PromptInput({ const wasBusyRef = React.useRef(busy); const hadFileMentionTokenRef = React.useRef(false); const appliedDraftNonceRef = React.useRef(null); + const pastesRef = React.useRef>(new Map()); + const pasteCounterRef = React.useRef(0); + // Track expanded paste regions for toggle (Ctrl+O expand / collapse). + const expandedRegionsRef = React.useRef>( + new Map() + ); const fileMentionToken = getCurrentFileMentionToken(buffer); const hasFileMentionToken = fileMentionToken !== null; @@ -170,16 +188,25 @@ export const PromptInput = React.memo(function PromptInput({ const showMenu = slashMenu.length > 0; const promptHistoryKey = React.useMemo(() => promptHistory.join("\0"), [promptHistory]); const hasRunningProcess = runningProcesses && runningProcesses.size > 0; - const processHint = hasRunningProcess ? " · ctrl+o view output" : ""; + const hasCollapsedMarkers = hasActivePasteMarkers(buffer.text); + const hasExpandedRegions = expandedRegionsRef.current.size > 0; + const processOrPasteHint = hasRunningProcess + ? " · ctrl+o view output" + : hasCollapsedMarkers + ? " · ctrl+o expand" + : hasExpandedRegions + ? " · ctrl+o collapse" + : ""; const footerText = statusMessage ? statusMessage : busy ? loadingText && loadingText.trim() - ? `${loadingText}${processHint}` - : `esc to interrupt · ctrl+c to cancel input${processHint}` - : `enter send · shift+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${processHint}`; + ? `${loadingText}${processOrPasteHint}` + : `esc to interrupt · ctrl+c to cancel input${processOrPasteHint}` + : `enter send · shift+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${processOrPasteHint}`; useTerminalFocusReporting(stdout, !disabled); useTerminalExtendedKeys(stdout, !disabled); + useBracketedPaste(stdout, !disabled); useHiddenTerminalCursor(stdout, !disabled); const refreshFileMentionItems = React.useCallback(() => { @@ -241,6 +268,8 @@ export const PromptInput = React.memo(function PromptInput({ setHistoryCursor(-1); setDraftBeforeHistory(null); clearPromptUndoRedoState(undoRedoRef.current); + pastesRef.current.clear(); + expandedRegionsRef.current.clear(); }, [promptDraft]); useEffect(() => { @@ -278,7 +307,7 @@ export const PromptInput = React.memo(function PromptInput({ if (runningProcesses && runningProcesses.size > 0 && onToggleProcessStdout) { onToggleProcessStdout(); } else { - setStatusMessage("No running process to inspect"); + expandPasteMarkerAtCursor(); } return; } @@ -306,6 +335,8 @@ export const PromptInput = React.memo(function PromptInput({ } else if (!isEmpty(buffer)) { setBuffer(EMPTY_BUFFER); clearUndoRedoStacks(); + pastesRef.current.clear(); + expandedRegionsRef.current.clear(); } else { setStatusMessage("press ctrl+d to exit"); } @@ -324,6 +355,11 @@ export const PromptInput = React.memo(function PromptInput({ exitHistoryBrowsing(); } + if (key.paste) { + handlePaste(input); + return; + } + if (key.ctrl && (input === "v" || input === "V")) { setStatusMessage("Reading clipboard..."); readClipboardImageAsync() @@ -395,12 +431,12 @@ export const PromptInput = React.memo(function PromptInput({ } if (key.delete) { - updateBuffer((s) => deleteForward(s)); + updateBuffer((s) => deletePasteMarkerForward(s) ?? deleteForward(s)); return; } if (key.backspace) { - updateBuffer((s) => backspace(s)); + updateBuffer((s) => deletePasteMarkerBackward(s) ?? backspace(s)); return; } @@ -490,6 +526,8 @@ export const PromptInput = React.memo(function PromptInput({ } if (key.ctrl && (input === "u" || input === "U")) { updateBuffer(() => EMPTY_BUFFER); + pastesRef.current.clear(); + expandedRegionsRef.current.clear(); return; } if (key.ctrl && (input === "w" || input === "W")) { @@ -567,6 +605,81 @@ export const PromptInput = React.memo(function PromptInput({ }); } + function handlePaste(pastedText: string): void { + const totalChars = pastedText.length; + + if (totalChars <= 1000) { + const newlineCount = (pastedText.match(/\n/g) ?? []).length; + if (newlineCount <= 9) { + const clean = pastedText + .replace(/\r\n|\r/g, "\n") + .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") + .replace(/\t/g, " "); + updateBuffer((s) => insertText(s, clean)); + return; + } + } + + // Large paste: store raw text, insert marker with line/char count. + const lineCount = (pastedText.match(/\n/g) ?? []).length + 1; + pasteCounterRef.current += 1; + const pasteId = pasteCounterRef.current; + pastesRef.current.set(pasteId, pastedText); + + const marker = + lineCount > 10 ? `[paste #${pasteId} +${lineCount} lines]` : `[paste #${pasteId} ${totalChars} chars]`; + + updateBuffer((s) => insertText(s, marker)); + } + + function expandPasteMarkerAtCursor(): void { + // First, try to collapse an already-expanded region at the cursor. + for (const [id, region] of expandedRegionsRef.current) { + if (buffer.cursor >= region.start && buffer.cursor <= region.end) { + // Collapse back to marker. + expandedRegionsRef.current.delete(id); + pastesRef.current.set(id, region.content); + setTimeout(() => { + updateBuffer((s) => { + const text = s.text.slice(0, region.start) + region.marker + s.text.slice(region.end); + return { text, cursor: region.start + region.marker.length }; + }); + }, 0); + return; + } + } + + // No expanded region at cursor — try to expand a paste marker. + const marker = findPasteMarkerContaining(buffer); + if (!marker) { + setStatusMessage("No paste marker at cursor"); + return; + } + const content = pastesRef.current.get(marker.id); + if (!content) { + setStatusMessage("Paste content not found"); + return; + } + + const pasteId = marker.id; + const originalMarker = buffer.text.slice(marker.start, marker.end); + pastesRef.current.delete(pasteId); + + setTimeout(() => { + updateBuffer((s) => { + const text = s.text.slice(0, marker.start) + cleanPasteContent(content) + s.text.slice(marker.end); + const newEnd = marker.start + content.length; + expandedRegionsRef.current.set(pasteId, { + start: marker.start, + end: newEnd, + content, + marker: originalMarker, + }); + return { text, cursor: marker.start }; + }); + }, 0); + } + function navigateHistory(direction: -1 | 1): void { if (promptHistory.length === 0) { return; @@ -607,6 +720,9 @@ export const PromptInput = React.memo(function PromptInput({ setImageUrls([]); setSelectedSkills([]); setShowSkillsDropdown(false); + pastesRef.current.clear(); + expandedRegionsRef.current.clear(); + pasteCounterRef.current = 0; } function handleSlashSelection(item: SlashCommandItem): void { @@ -664,6 +780,8 @@ export const PromptInput = React.memo(function PromptInput({ setImageUrls([]); setSelectedSkills([]); setShowSkillsDropdown(false); + pastesRef.current.clear(); + expandedRegionsRef.current.clear(); return; } if (item.kind === "mcp") { @@ -699,7 +817,7 @@ export const PromptInput = React.memo(function PromptInput({ } onSubmit({ - text: buffer.text, + text: expandPasteMarkers(buffer.text, pastesRef.current), imageUrls, selectedSkills, }); @@ -871,9 +989,6 @@ export function getPromptReturnKeyAction(key: Pick= end) return ""; + + const segText = text.slice(start, end); + const cursorRel = cursor - start; // relative cursor position inside this segment + + // Cursor not in this segment – just return the text. + if (cursorRel < 0 || cursorRel > segText.length) { + return highlighted ? chalk.yellow(segText) : segText; } - if (typeof at === "undefined") { - return before + renderCursorCell(" "); + // Cursor is exactly at `end` (which equals `segText.length`). + if (cursorRel === segText.length) { + return highlighted ? chalk.yellow(segText) + renderCursorCell(" ") : segText + renderCursorCell(" "); } + + // Cursor is somewhere inside the segment. + const at = segText[cursorRel]; + if (at === "\n") { + // Render newline as a space in the cursor cell, then output the actual newline. + const before = segText.slice(0, cursorRel); + const after = segText.slice(cursorRel + 1); return before + renderCursorCell(" ") + "\n" + after; } + + const before = segText.slice(0, cursorRel); + const after = segText.slice(cursorRel + 1); + if (highlighted) { + return chalk.yellow(before) + renderCursorCell(at) + chalk.yellow(after); + } return before + renderCursorCell(at) + after; } diff --git a/src/ui/prompt/cursor.ts b/src/ui/prompt/cursor.ts index 2668470c..aefea342 100644 --- a/src/ui/prompt/cursor.ts +++ b/src/ui/prompt/cursor.ts @@ -40,6 +40,14 @@ function disableTerminalFocusReporting(): string { return "\u001B[?1004l"; } +function enableBracketedPaste(): string { + return "\u001B[?2004h"; +} + +function disableBracketedPaste(): string { + return "\u001B[?2004l"; +} + export function enableTerminalExtendedKeys(): string { return "\u001B[>4;1m"; } @@ -260,3 +268,16 @@ export function useTerminalExtendedKeys(stdout: NodeJS.WriteStream | undefined, }; }, [isActive, stdout]); } + +export function useBracketedPaste(stdout: NodeJS.WriteStream | undefined, isActive: boolean): void { + useLayoutEffect(() => { + if (!isActive || !stdout?.isTTY) { + return; + } + + stdout.write(enableBracketedPaste()); + return () => { + stdout.write(disableBracketedPaste()); + }; + }, [isActive, stdout]); +} diff --git a/src/ui/prompt/index.ts b/src/ui/prompt/index.ts index 59075581..6435f620 100644 --- a/src/ui/prompt/index.ts +++ b/src/ui/prompt/index.ts @@ -4,6 +4,7 @@ export type { InputKey } from "./useTerminalInput"; export { useHiddenTerminalCursor, useTerminalExtendedKeys, + useBracketedPaste, usePromptTerminalCursor, useTerminalFocusReporting, getPromptCursorPlacement, diff --git a/src/ui/prompt/useTerminalInput.ts b/src/ui/prompt/useTerminalInput.ts index 9ce69766..e3d63491 100644 --- a/src/ui/prompt/useTerminalInput.ts +++ b/src/ui/prompt/useTerminalInput.ts @@ -20,6 +20,8 @@ export type InputKey = { meta: boolean; focusIn: boolean; focusOut: boolean; + /** True when the input came from a bracketed paste (ESC[200~ ... ESC[201~). */ + paste: boolean; }; const BACKSPACE_BYTES = new Set(["\u007F", "\b"]); @@ -35,6 +37,13 @@ const META_RIGHT_SEQUENCES = new Set(["\u001B[1;3C", "\u001B[3C", "\u001Bf"]); const TERMINAL_FOCUS_IN = "\u001B[I"; const TERMINAL_FOCUS_OUT = "\u001B[O"; +// Bracketed paste mode markers (xterm-style). +// When the terminal supports bracketed paste, pasted text is wrapped with: +// ESC[200~ ...pasted content... ESC[201~ +const PASTE_START = "\u001B[200~"; +const PASTE_END = "\u001B[201~"; +const PASTE_END_LENGTH = 6; // length of PASTE_END + // Ctrl+- (minus) sequences in modifyOtherKeys mode. // \u001B[45;5u — standard format: keycode=45 ('-'), modifier=5 (Ctrl) // \u001B[27;5;45~ — extended format for function-like reporting @@ -73,6 +82,7 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key: meta: false, focusIn: false, focusOut: false, + paste: false, }; return { input, key }; } @@ -100,6 +110,7 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key: meta: false, focusIn: false, focusOut: false, + paste: false, }; return { input, key }; } @@ -123,6 +134,7 @@ export function parseTerminalInput(data: Buffer | string): { input: string; key: meta: META_LEFT_SEQUENCES.has(raw) || META_RIGHT_SEQUENCES.has(raw) || META_RETURN_SEQUENCES.has(raw), focusIn: raw === TERMINAL_FOCUS_IN, focusOut: raw === TERMINAL_FOCUS_OUT, + paste: false, }; if (input <= "\u001A" && !key.return) { @@ -200,6 +212,29 @@ export function dispatchTerminalInput( inputHandler(input, key); } +/** An InputKey with all fields false (including paste). Used when dispatching paste events. */ +const EMPTY_KEY: InputKey = { + upArrow: false, + downArrow: false, + leftArrow: false, + rightArrow: false, + home: false, + end: false, + pageDown: false, + pageUp: false, + return: false, + escape: false, + ctrl: false, + shift: false, + tab: false, + backspace: false, + delete: false, + meta: false, + focusIn: false, + focusOut: false, + paste: false, +}; + export function useTerminalInput( inputHandler: (input: string, key: InputKey) => void, options: { isActive?: boolean } = {} @@ -209,8 +244,15 @@ export function useTerminalInput( const handlerRef = useRef(inputHandler); handlerRef.current = inputHandler; + // Mutable paste-bracketing state shared across data events. + // Uses an array of chunks instead of string concatenation to avoid + // O(n²) copying when the terminal splits a large paste across many events. + const pasteRef = useRef({ active: false, chunks: [] as string[] }); + useEffect(() => { if (!isActive) { + pasteRef.current.active = false; + pasteRef.current.chunks = []; return; } setRawMode(true); @@ -223,7 +265,75 @@ export function useTerminalInput( if (!isActive) { return; } + const handleData = (data: Buffer | string) => { + const raw = String(data); + + // ----- Bracketed paste handling ----- + // Most terminals send the start/end markers in the same chunk as + // the content. We handle both inline and multi-chunk scenarios. + + if (raw.includes(PASTE_START)) { + pasteRef.current.active = true; + pasteRef.current.chunks = []; + + // Extract content after the start marker. + const startIdx = raw.indexOf(PASTE_START); + const afterStart = raw.slice(startIdx + PASTE_START.length); + + // Check if the end marker is also in this same chunk. + const endIdx = afterStart.indexOf(PASTE_END); + if (endIdx !== -1) { + // Both markers in one chunk — process immediately. + const pasteContent = afterStart.slice(0, endIdx); + pasteRef.current.active = false; + const remaining = afterStart.slice(endIdx + PASTE_END_LENGTH); + + if (pasteContent.length > 0) { + handlerRef.current(pasteContent, { ...EMPTY_KEY, paste: true }); + } + if (remaining.length > 0) { + dispatchTerminalInput(remaining, handlerRef.current); + } + return; + } + + // Only start marker — buffer as first chunk. + if (afterStart) { + pasteRef.current.chunks.push(afterStart); + } + return; + } + + if (pasteRef.current.active) { + pasteRef.current.chunks.push(raw); + // Only join+search when this chunk might contain the end marker. + if (raw.includes("201~")) { + const combined = pasteRef.current.chunks.join(""); + const endIdx = combined.indexOf(PASTE_END); + if (endIdx !== -1) { + const pasteContent = combined.slice(0, endIdx); + pasteRef.current.active = false; + const remaining = combined.slice(endIdx + PASTE_END_LENGTH); + pasteRef.current.chunks = []; + + // Dispatch the pasted text as a single event. + if (pasteContent.length > 0) { + handlerRef.current(pasteContent, { ...EMPTY_KEY, paste: true }); + } + + // Handle any remaining input after the paste end marker. + if (remaining.length > 0) { + dispatchTerminalInput(remaining, handlerRef.current); + } + return; + } + return; + } + return; + } + + // ----- Normal (non-paste) input ----- dispatchTerminalInput(data, handlerRef.current); }; diff --git a/src/ui/promptBuffer.ts b/src/ui/promptBuffer.ts index 3e3c1827..97d15a51 100644 --- a/src/ui/promptBuffer.ts +++ b/src/ui/promptBuffer.ts @@ -171,6 +171,120 @@ export function getCurrentSlashToken(state: PromptBufferState): string | null { return line; } +/** + * Regex matching paste markers like `[paste #1 +123 lines]` or `[paste #2 1234 chars]`. + * When the user pastes a large block of text (>10 lines or >1000 chars), a compact + * marker is inserted instead of the full content. The actual content is stored in a + * Map and expanded back before submission. + */ +export const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+?\d+ lines|\d+ chars))?\]/g; + +/** + * Find the paste marker that ends exactly at `state.cursor`, if any. + * Returns the marker's start and end positions, or `null`. + */ +export function findPasteMarkerBefore(state: PromptBufferState): { start: number; end: number } | null { + // Walk backwards through all markers and return the one that ends at the cursor. + let match: RegExpExecArray | null; + PASTE_MARKER_REGEX.lastIndex = 0; + while ((match = PASTE_MARKER_REGEX.exec(state.text)) !== null) { + if (match.index + match[0].length === state.cursor) { + return { start: match.index, end: match.index + match[0].length }; + } + } + return null; +} + +/** + * Find the paste marker that starts exactly at `state.cursor`, if any. + * Returns the marker's start and end positions, or `null`. + */ +export function findPasteMarkerAt(state: PromptBufferState): { start: number; end: number } | null { + let match: RegExpExecArray | null; + PASTE_MARKER_REGEX.lastIndex = 0; + while ((match = PASTE_MARKER_REGEX.exec(state.text)) !== null) { + if (match.index === state.cursor) { + return { start: match.index, end: match.index + match[0].length }; + } + } + return null; +} + +/** + * If the cursor is immediately after a paste marker, delete the entire marker + * (atomic backspace). Returns the new state, or `state` unchanged if no marker. + */ +export function deletePasteMarkerBackward(state: PromptBufferState): PromptBufferState | null { + const marker = findPasteMarkerBefore(state); + if (!marker) return null; + const text = state.text.slice(0, marker.start) + state.text.slice(marker.end); + return { text, cursor: marker.start }; +} + +/** + * If the cursor is at the start of a paste marker, delete the entire marker + * (atomic forward delete). Returns the new state, or `state` unchanged if no marker. + */ +export function deletePasteMarkerForward(state: PromptBufferState): PromptBufferState | null { + const marker = findPasteMarkerAt(state); + if (!marker) return null; + const text = state.text.slice(0, marker.start) + state.text.slice(marker.end); + return { text, cursor: marker.start }; +} + +/** + * Sanitize stored paste content (filter control chars, expand tabs). + * Called lazily on expand/submit, not during paste to keep paste instant. + */ +export function cleanPasteContent(text: string): string { + return text + .replace(/\r\n|\r/g, "\n") + .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") + .replace(/\t/g, " "); +} + +/** + * Expand paste markers in the text back to their original (cleaned) content. + * @param text - Text potentially containing paste markers. + * @param pastes - Map of paste ID → original content. + */ +export function expandPasteMarkers(text: string, pastes: Map): string { + if (pastes.size === 0) return text; + let result = text; + for (const [pasteId, pasteContent] of pastes) { + const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+?\\d+ lines|\\d+ chars))?\\]`, "g"); + result = result.replace(markerRegex, () => cleanPasteContent(pasteContent)); + } + return result; +} + +/** + * Find the paste marker that contains `state.cursor`, if any. + * Returns the marker's start, end, and numeric paste ID, or `null`. + */ +export function findPasteMarkerContaining(state: PromptBufferState): { start: number; end: number; id: number } | null { + let match: RegExpExecArray | null; + PASTE_MARKER_REGEX.lastIndex = 0; + while ((match = PASTE_MARKER_REGEX.exec(state.text)) !== null) { + if (match.index <= state.cursor && match.index + match[0].length >= state.cursor) { + return { + start: match.index, + end: match.index + match[0].length, + id: Number.parseInt(match[1]!, 10), + }; + } + } + return null; +} + +/** + * Check whether the given text contains any paste markers. + */ +export function hasActivePasteMarkers(text: string): boolean { + PASTE_MARKER_REGEX.lastIndex = 0; + return PASTE_MARKER_REGEX.test(text); +} + function locate(state: PromptBufferState): { line: number; column: number; From bb95daff1995dabf191e7888909423e5018e71a3 Mon Sep 17 00:00:00 2001 From: Kayro Date: Wed, 20 May 2026 22:30:59 +0800 Subject: [PATCH 049/212] fix: validate paste markers by ID to prevent false positives - hasActivePasteMarkers now checks validIds map, not just regex match - deletePasteMarkerBackward/Forward only atomically delete real paste markers - renderBufferWithCursor and renderFocusedText only highlight markers with valid IDs - PASTE_MARKER_REGEX requires line/char suffix (no bare [paste #N]) - Fix empty buffer cursor rendering in renderFocusedText regression - All render/test call sites updated to pass pastesRef.current --- src/ui/PromptInput.tsx | 42 +++++++++++++++++++++++++----------------- src/ui/promptBuffer.ts | 35 ++++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index ad73d838..0eaa1691 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -188,7 +188,7 @@ export const PromptInput = React.memo(function PromptInput({ const showMenu = slashMenu.length > 0; const promptHistoryKey = React.useMemo(() => promptHistory.join("\0"), [promptHistory]); const hasRunningProcess = runningProcesses && runningProcesses.size > 0; - const hasCollapsedMarkers = hasActivePasteMarkers(buffer.text); + const hasCollapsedMarkers = hasActivePasteMarkers(buffer.text, pastesRef.current); const hasExpandedRegions = expandedRegionsRef.current.size > 0; const processOrPasteHint = hasRunningProcess ? " · ctrl+o view output" @@ -431,12 +431,12 @@ export const PromptInput = React.memo(function PromptInput({ } if (key.delete) { - updateBuffer((s) => deletePasteMarkerForward(s) ?? deleteForward(s)); + updateBuffer((s) => deletePasteMarkerForward(s, pastesRef.current) ?? deleteForward(s)); return; } if (key.backspace) { - updateBuffer((s) => deletePasteMarkerBackward(s) ?? backspace(s)); + updateBuffer((s) => deletePasteMarkerBackward(s, pastesRef.current) ?? backspace(s)); return; } @@ -872,7 +872,7 @@ export const PromptInput = React.memo(function PromptInput({ borderDimColor > - {renderBufferWithCursor(buffer, !disabled && hasTerminalFocus, placeholder)} + {renderBufferWithCursor(buffer, !disabled && hasTerminalFocus, placeholder, pastesRef.current)} {inlineHint ? {inlineHint} : null} +): string { const text = state.text || ""; const cursor = Math.max(0, Math.min(state.cursor, text.length)); + const validIds = validPastes ?? new Map(); if (text.length === 0 && placeholder) { if (!isFocused) { @@ -997,18 +1003,18 @@ export function renderBufferWithCursor(state: PromptBufferState, isFocused: bool return renderCursorCell(" ") + chalk.dim(` ${placeholder}`); } + if (text.length === 0) { + return isFocused ? renderCursorCell(" ") : ""; + } + if (!isFocused) { - return highlightPasteMarkersInText(text); + return highlightPasteMarkersInText(text, validIds); } - // Focused: scan through the text, highlight paste markers, and insert - // the cursor cell at the correct position. This approach handles the - // case where the cursor sits at the start of (or inside) a paste marker. - return renderFocusedText(text, cursor); + return renderFocusedText(text, cursor, validIds); } -/** Highlight paste markers in a plain string (no cursor). */ -function highlightPasteMarkersInText(s: string): string { +function highlightPasteMarkersInText(s: string, validIds: Map): string { if (!s.includes("[paste #")) return s; PASTE_MARKER_REGEX.lastIndex = 0; let result = ""; @@ -1016,7 +1022,8 @@ function highlightPasteMarkersInText(s: string): string { let match: RegExpExecArray | null; while ((match = PASTE_MARKER_REGEX.exec(s)) !== null) { result += s.slice(pos, match.index); - result += chalk.yellow(match[0]); + const id = Number.parseInt(match[1]!, 10); + result += validIds.has(id) ? chalk.yellow(match[0]) : match[0]; pos = match.index + match[0].length; } result += s.slice(pos); @@ -1029,7 +1036,7 @@ function highlightPasteMarkersInText(s: string): string { * anywhere (including inside or at the boundary of a paste marker) and the * marker will still be highlighted correctly. */ -function renderFocusedText(text: string, cursor: number): string { +function renderFocusedText(text: string, cursor: number, validIds: Map): string { let result = ""; let pos = 0; PASTE_MARKER_REGEX.lastIndex = 0; @@ -1038,14 +1045,15 @@ function renderFocusedText(text: string, cursor: number): string { while ((match = PASTE_MARKER_REGEX.exec(text)) !== null) { const markerStart = match.index; const markerEnd = match.index + match[0].length; + const id = Number.parseInt(match[1]!, 10); + const isReal = validIds.has(id); // 1. Non-marker segment before this marker. result += renderTextSegmentWithCursor(text, pos, markerStart, cursor, false); pos = markerStart; - // 2. Marker segment — highlighted with chalk.yellow. - // The cursor may fall inside it. - result += renderTextSegmentWithCursor(text, pos, markerEnd, cursor, true); + // 2. Marker segment — highlighted only if it corresponds to a real paste. + result += renderTextSegmentWithCursor(text, pos, markerEnd, cursor, isReal); pos = markerEnd; } diff --git a/src/ui/promptBuffer.ts b/src/ui/promptBuffer.ts index 97d15a51..3e0a710b 100644 --- a/src/ui/promptBuffer.ts +++ b/src/ui/promptBuffer.ts @@ -177,7 +177,7 @@ export function getCurrentSlashToken(state: PromptBufferState): string | null { * marker is inserted instead of the full content. The actual content is stored in a * Map and expanded back before submission. */ -export const PASTE_MARKER_REGEX = /\[paste #(\d+)( (\+?\d+ lines|\d+ chars))?\]/g; +export const PASTE_MARKER_REGEX = /\[paste #(\d+) (\+?\d+ lines|\d+ chars)\]/g; /** * Find the paste marker that ends exactly at `state.cursor`, if any. @@ -214,9 +214,16 @@ export function findPasteMarkerAt(state: PromptBufferState): { start: number; en * If the cursor is immediately after a paste marker, delete the entire marker * (atomic backspace). Returns the new state, or `state` unchanged if no marker. */ -export function deletePasteMarkerBackward(state: PromptBufferState): PromptBufferState | null { +export function deletePasteMarkerBackward( + state: PromptBufferState, + validIds: Map +): PromptBufferState | null { const marker = findPasteMarkerBefore(state); if (!marker) return null; + // Only delete if this is a real paste marker (ID in validIds). + PASTE_MARKER_REGEX.lastIndex = 0; + const m = PASTE_MARKER_REGEX.exec(state.text.slice(marker.start, marker.end)); + if (!m || !validIds.has(Number.parseInt(m[1]!, 10))) return null; const text = state.text.slice(0, marker.start) + state.text.slice(marker.end); return { text, cursor: marker.start }; } @@ -225,9 +232,16 @@ export function deletePasteMarkerBackward(state: PromptBufferState): PromptBuffe * If the cursor is at the start of a paste marker, delete the entire marker * (atomic forward delete). Returns the new state, or `state` unchanged if no marker. */ -export function deletePasteMarkerForward(state: PromptBufferState): PromptBufferState | null { +export function deletePasteMarkerForward( + state: PromptBufferState, + validIds: Map +): PromptBufferState | null { const marker = findPasteMarkerAt(state); if (!marker) return null; + // Only delete if this is a real paste marker (ID in validIds). + PASTE_MARKER_REGEX.lastIndex = 0; + const m = PASTE_MARKER_REGEX.exec(state.text.slice(marker.start, marker.end)); + if (!m || !validIds.has(Number.parseInt(m[1]!, 10))) return null; const text = state.text.slice(0, marker.start) + state.text.slice(marker.end); return { text, cursor: marker.start }; } @@ -252,7 +266,7 @@ export function expandPasteMarkers(text: string, pastes: Map): s if (pastes.size === 0) return text; let result = text; for (const [pasteId, pasteContent] of pastes) { - const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+?\\d+ lines|\\d+ chars))?\\]`, "g"); + const markerRegex = new RegExp(`\\[paste #${pasteId} (\\+?\\d+ lines|\\d+ chars)\\]`, "g"); result = result.replace(markerRegex, () => cleanPasteContent(pasteContent)); } return result; @@ -278,11 +292,18 @@ export function findPasteMarkerContaining(state: PromptBufferState): { start: nu } /** - * Check whether the given text contains any paste markers. + * Check whether the text contains real paste markers (IDs present in validIds). */ -export function hasActivePasteMarkers(text: string): boolean { +export function hasActivePasteMarkers(text: string, validIds: Map): boolean { + if (!text.includes("[paste #")) return false; PASTE_MARKER_REGEX.lastIndex = 0; - return PASTE_MARKER_REGEX.test(text); + let match: RegExpExecArray | null; + while ((match = PASTE_MARKER_REGEX.exec(text)) !== null) { + if (validIds.has(Number.parseInt(match[1]!, 10))) { + return true; + } + } + return false; } function locate(state: PromptBufferState): { From 27fcfd02e0c8cf564f39fa8d3fc4c9a878dbe501 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 21 May 2026 10:36:19 +0800 Subject: [PATCH 050/212] feat: add normalizeLlmToolCalls() to replace missing/empty tool call IDs --- src/session.ts | 34 ++++++++++++++++++++-- src/tests/session.test.ts | 60 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/session.ts b/src/session.ts index b4a3c719..54340e72 100644 --- a/src/session.ts +++ b/src/session.ts @@ -570,9 +570,10 @@ export class SessionManager { const toolCalls = Array.from(toolCallsByIndex.entries()) .sort(([left], [right]) => left - right) .map(([, toolCall]) => toolCall); + const normalizedToolCalls = this.normalizeLlmToolCalls(toolCalls); const message: Record = { content }; - if (toolCalls.length > 0) { - message.tool_calls = toolCalls; + if (normalizedToolCalls) { + message.tool_calls = normalizedToolCalls; } if (reasoningContent.length > 0) { message.reasoning_content = reasoningContent; @@ -1180,7 +1181,7 @@ ${skillMd} const rawContent = message?.content; const content = typeof rawContent === "string" ? rawContent : ""; const rawToolCalls = (message as { tool_calls?: unknown[] } | undefined)?.tool_calls ?? null; - toolCalls = Array.isArray(rawToolCalls) && rawToolCalls.length > 0 ? rawToolCalls : null; + toolCalls = this.normalizeLlmToolCalls(rawToolCalls); const rawThinking = (message as { reasoning_content?: unknown } | undefined)?.reasoning_content; const thinking = typeof rawThinking === "string" ? rawThinking : null; const refusal = (message as { refusal?: string } | undefined)?.refusal ?? null; @@ -1899,6 +1900,33 @@ ${skillMd} }; } + private generateToolCallId(): string { + return crypto.randomBytes(16).toString("hex"); + } + + private normalizeLlmToolCalls(rawToolCalls: unknown[] | null | undefined): unknown[] | null { + if (!Array.isArray(rawToolCalls) || rawToolCalls.length === 0) { + return null; + } + + return rawToolCalls.map((toolCall) => { + if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) { + return toolCall; + } + + const record = toolCall as Record; + const id = typeof record.id === "string" ? record.id.trim() : ""; + if (id) { + return toolCall; + } + + return { + ...record, + id: this.generateToolCallId(), + }; + }); + } + private buildToolMessage( sessionId: string, toolCallId: string, diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 9f3c7fb5..fd831990 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1565,6 +1565,66 @@ test("Write tool params prefer file_path even when content appears first", () => assert.equal(toolMessage.meta?.paramsMd, filePath); }); +test("LLM tool calls without ids receive generated 32 character ids", async () => { + const workspace = createTempDir("deepcode-tool-call-id-workspace-"); + const home = createTempDir("deepcode-tool-call-id-home-"); + setHomeDir(home); + + const filePath = path.join(workspace, "note.txt"); + fs.writeFileSync(filePath, "hello\n", "utf8"); + const plan = "## Task List\n\n- [ ] Inspect current behavior"; + const manager = createMockedClientSessionManager(workspace, [ + { + choices: [ + { + message: { + content: "", + tool_calls: [ + { + id: "", + type: "function", + function: { + name: "UpdatePlan", + arguments: JSON.stringify({ plan, explanation: "Initial plan" }), + }, + }, + { + type: "function", + function: { + name: "read", + arguments: JSON.stringify({ file_path: filePath }), + }, + }, + ], + }, + }, + ], + }, + createChatResponse("done", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }), + ]); + + const sessionId = await manager.createSession({ text: "inspect note" }); + const assistantMessage = manager + .listSessionMessages(sessionId) + .find((message) => message.role === "assistant" && (message.messageParams as any)?.tool_calls); + const toolCalls = (assistantMessage?.messageParams as { tool_calls?: Array<{ id?: unknown }> } | null)?.tool_calls; + + assert.equal(toolCalls?.length, 2); + assert.match(String(toolCalls?.[0]?.id), /^[0-9a-f]{32}$/); + assert.match(String(toolCalls?.[1]?.id), /^[0-9a-f]{32}$/); + assert.notEqual(toolCalls?.[0]?.id, toolCalls?.[1]?.id); + + const toolMessages = manager.listSessionMessages(sessionId).filter((message) => message.role === "tool"); + assert.deepEqual( + toolMessages.map((message) => (message.messageParams as { tool_call_id?: unknown } | null)?.tool_call_id), + toolCalls?.map((toolCall) => toolCall.id) + ); + + const readToolMessage = toolMessages.find((message) => JSON.parse(message.content ?? "{}").name === "read"); + assert.equal((readToolMessage?.meta?.function as { name?: string } | undefined)?.name, "read"); + assert.equal(readToolMessage?.meta?.paramsMd, "note.txt"); +}); + test("buildOpenAIMessages repairs mixed missing duplicate and orphan tool messages", () => { const manager = createSessionManager(process.cwd(), "machine-id-mixed-tool-badcase"); const assistantMessage = (manager as any).buildAssistantMessage( From 5b51f4066c09764eb1de631109166a8d69013a94 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 21 May 2026 10:40:31 +0800 Subject: [PATCH 051/212] 0.1.24 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17a77cae..0250531e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.23", + "version": "0.1.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.23", + "version": "0.1.24", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index b72fd96a..c8809ea1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.23", + "version": "0.1.24", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From ec9a219554858262687180b40f5726a133e3bf73 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 21 May 2026 14:41:24 +0800 Subject: [PATCH 052/212] =?UTF-8?q?docs(cli):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=B9=B6=E6=89=A9=E5=B1=95=E5=91=BD=E4=BB=A4=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E5=92=8C=E5=B8=AE=E5=8A=A9=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /skills、/model、/undo、/mcp 和 /raw 命令说明 - README.md 和 README-en.md 中同步添加对应命令描述 - 详细列出各命令功能,方便用户快速查阅 - 修改 CLI 界面帮助输出,包含所有新增命令提示 - 优化菜单结构,提升用户操作体验 --- README-en.md | 8 ++++++++ README-zh_CN.md | 45 ++++++++++++++++++++++++++------------------- README.md | 45 ++++++++++++++++++++++++++------------------- src/cli.tsx | 5 +++++ 4 files changed, 65 insertions(+), 38 deletions(-) diff --git a/README-en.md b/README-en.md index 55d0cf69..d9719fd3 100644 --- a/README-en.md +++ b/README-en.md @@ -66,11 +66,13 @@ Deep Code CLI supports agent skills that allow you to extend the assistant's cap | `/` | Open the skills / commands menu | | `/new` | Start a fresh conversation | | `/resume` | Choose a previous conversation to continue | +| `/continue` | Continue the active conversation or pick one to resume | | `/model` | Switch model, thinking mode, and reasoning effort | | `/raw` | Toggle display mode (Normal / Lite / Raw scrollback) | | `/init` | Initialize an AGENTS.md file (LLM project instructions) | | `/skills` | List available skills | | `/mcp` | View MCP server status and available tools | +| `/undo` | Restore code and/or conversation to a previous point | | `/exit` | Quit (also `Ctrl+D` twice) | | Key | Action | @@ -126,6 +128,12 @@ Deep Code supports MCP (Model Context Protocol) to connect external services suc For detailed setup instructions, see: [docs/mcp.md](docs/mcp.md) +### How to configure Deep Code to send notifications after a task completes? + +When the AI assistant completes a task, Deep Code can automatically execute a notification script to send the task results to the specified channel (e.g., Slack, system notifications, etc.). + +For detailed configuration instructions, see: [docs/notify_en.md](docs/notify_en.md) + ## Contributing Contributions are welcome! Here's how to get started: diff --git a/README-zh_CN.md b/README-zh_CN.md index 8a427def..7b74a502 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -60,25 +60,27 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力: ## 斜杠命令与按键功能 -| 斜杠命令 | 操作 | -|-----------------|---------------------------------------------| -| `/` | 打开 skills / 命令菜单 | -| `/new` | 开始新对话 | -| `/resume` | 选择历史对话继续 | -| `/model` | 切换模型、思考模式和推理强度 | -| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)| -| `/init` | 初始化 AGENTS.md 文件 | -| `/skills` | 列出可用 skills | -| `/mcp` | 查看 MCP 服务器状态和可用工具 | -| `/exit` | 退出(也可用连续 `Ctrl+D`) | - -| 按键 | 操作 | -|-----------------|---------------------------------------------| -| `Enter` | 发送消息 | -| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | -| `Ctrl+V` | 从剪贴板粘贴图片 | -| `Esc` | 中断当前模型回复 | -| 连续 `Ctrl+D` | 退出 | +| 斜杠命令 | 操作 | +|-------------|----------------------------------| +| `/` | 打开 skills / 命令菜单 | +| `/new` | 开始新对话 | +| `/resume` | 选择历史对话继续 | +| `/continue` | 继续当前对话,或选择历史对话恢复 | +| `/model` | 切换模型、思考模式和推理强度 | +| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯) | +| `/init` | 初始化 AGENTS.md 文件 | +| `/skills` | 列出可用 skills | +| `/mcp` | 查看 MCP 服务器状态和可用工具 | +| `/undo` | 将代码和/或对话恢复到之前的状态 | +| `/exit` | 退出(也可用连续 `Ctrl+D`) | + +| 按键 | 操作 | +|---------------|--------------------| +| `Enter` | 发送消息 | +| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | +| `Ctrl+V` | 从剪贴板粘贴图片 | +| `Esc` | 中断当前模型回复 | +| 连续 `Ctrl+D` | 退出 | ## 支持的模型 @@ -111,6 +113,11 @@ Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览 详细配置指南:[docs/mcp.md](docs/mcp.md) +### 如何配置 Deep Code 任务完成后发送通知? + +当 AI 助手完成一轮任务后,Deep Code 可以自动执行一个通知脚本,将任务结果发送到你指定的渠道(如 Slack、系统通知等)。 + +详细配置指南:[docs/notify.md](docs/notify.md) ### 是否支持 Coding Plan? diff --git a/README.md b/README.md index 8a427def..7b74a502 100644 --- a/README.md +++ b/README.md @@ -60,25 +60,27 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力: ## 斜杠命令与按键功能 -| 斜杠命令 | 操作 | -|-----------------|---------------------------------------------| -| `/` | 打开 skills / 命令菜单 | -| `/new` | 开始新对话 | -| `/resume` | 选择历史对话继续 | -| `/model` | 切换模型、思考模式和推理强度 | -| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)| -| `/init` | 初始化 AGENTS.md 文件 | -| `/skills` | 列出可用 skills | -| `/mcp` | 查看 MCP 服务器状态和可用工具 | -| `/exit` | 退出(也可用连续 `Ctrl+D`) | - -| 按键 | 操作 | -|-----------------|---------------------------------------------| -| `Enter` | 发送消息 | -| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | -| `Ctrl+V` | 从剪贴板粘贴图片 | -| `Esc` | 中断当前模型回复 | -| 连续 `Ctrl+D` | 退出 | +| 斜杠命令 | 操作 | +|-------------|----------------------------------| +| `/` | 打开 skills / 命令菜单 | +| `/new` | 开始新对话 | +| `/resume` | 选择历史对话继续 | +| `/continue` | 继续当前对话,或选择历史对话恢复 | +| `/model` | 切换模型、思考模式和推理强度 | +| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯) | +| `/init` | 初始化 AGENTS.md 文件 | +| `/skills` | 列出可用 skills | +| `/mcp` | 查看 MCP 服务器状态和可用工具 | +| `/undo` | 将代码和/或对话恢复到之前的状态 | +| `/exit` | 退出(也可用连续 `Ctrl+D`) | + +| 按键 | 操作 | +|---------------|--------------------| +| `Enter` | 发送消息 | +| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | +| `Ctrl+V` | 从剪贴板粘贴图片 | +| `Esc` | 中断当前模型回复 | +| 连续 `Ctrl+D` | 退出 | ## 支持的模型 @@ -111,6 +113,11 @@ Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览 详细配置指南:[docs/mcp.md](docs/mcp.md) +### 如何配置 Deep Code 任务完成后发送通知? + +当 AI 助手完成一轮任务后,Deep Code 可以自动执行一个通知脚本,将任务结果发送到你指定的渠道(如 Slack、系统通知等)。 + +详细配置指南:[docs/notify.md](docs/notify.md) ### 是否支持 Coding Plan? diff --git a/src/cli.tsx b/src/cli.tsx index 66ceb7d8..c3876ae5 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -41,10 +41,15 @@ if (args.includes("--help") || args.includes("-h")) { " ctrl+x Clear pasted images", " esc Interrupt the current model turn", " / Open the skills/commands menu", + " /skills List available skills", + " /model Select model, thinking mode and effort control", " /new Start a fresh conversation", " /init Initialize an AGENTS.md file with instructions for LLM", " /resume Pick a previous conversation to continue", " /continue Continue the active conversation, or resume one if empty", + " /undo Restore code and/or conversation to a previous point", + " /mcp Show MCP server status and available tools", + " /raw Toggle display mode for viewing or collapsing reasoning content", " /exit Quit", " ctrl+d twice Quit", ].join("\n") + "\n" From 040a3245bbca60301c325ce6b993bf236ed8df4c Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 21 May 2026 14:43:08 +0800 Subject: [PATCH 053/212] =?UTF-8?q?docs(cli):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=B9=B6=E6=89=A9=E5=B1=95=E5=91=BD=E4=BB=A4=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E5=92=8C=E5=B8=AE=E5=8A=A9=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /skills、/model、/undo、/mcp 和 /raw 命令说明 - README.md 和 README-en.md 中同步添加对应命令描述 - 详细列出各命令功能,方便用户快速查阅 - 修改 CLI 界面帮助输出,包含所有新增命令提示 - 优化菜单结构,提升用户操作体验 --- README-en.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README-en.md b/README-en.md index d9719fd3..18e3f139 100644 --- a/README-en.md +++ b/README-en.md @@ -61,19 +61,19 @@ Deep Code CLI supports agent skills that allow you to extend the assistant's cap ## Slash Commands & Keyboard Shortcuts -| Slash Command | Action | -|------------------|----------------------------------------------------------| -| `/` | Open the skills / commands menu | -| `/new` | Start a fresh conversation | -| `/resume` | Choose a previous conversation to continue | -| `/continue` | Continue the active conversation or pick one to resume | -| `/model` | Switch model, thinking mode, and reasoning effort | -| `/raw` | Toggle display mode (Normal / Lite / Raw scrollback) | -| `/init` | Initialize an AGENTS.md file (LLM project instructions) | -| `/skills` | List available skills | -| `/mcp` | View MCP server status and available tools | -| `/undo` | Restore code and/or conversation to a previous point | -| `/exit` | Quit (also `Ctrl+D` twice) | +| Slash Command | Action | +|------------------|---------------------------------------------------------| +| `/` | Open the skills / commands menu | +| `/new` | Start a fresh conversation | +| `/resume` | Choose a previous conversation to continue | +| `/continue` | Continue the active conversation or pick one to resume | +| `/model` | Switch model, thinking mode, and reasoning effort | +| `/raw` | Toggle display mode (Normal / Lite / Raw scrollback) | +| `/init` | Initialize an AGENTS.md file (LLM project instructions) | +| `/skills` | List available skills | +| `/mcp` | View MCP server status and available tools | +| `/undo` | Restore code and/or conversation to a previous point | +| `/exit` | Quit (also `Ctrl+D` twice) | | Key | Action | |------------------|----------------------------------------------------------| From 56e75050dce95fadf4bfde76feb80d703699a2a1 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 21 May 2026 15:37:55 +0800 Subject: [PATCH 054/212] =?UTF-8?q?docs(readme):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=BE=BD=E7=AB=A0=E5=B1=95=E7=A4=BA=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E4=BF=A1=E6=81=AF=E5=8F=AF=E8=A7=81=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 README.md、README-en.md 和 README-zh_CN.md 中添加了 npm 和 GitHub 相关徽章 - 新增版本号、下载量、贡献者、分支、Star、Issue、PR 和许可信息的动态展示链接 - 增强项目主页的视觉效果和信息传达 - 为中英文 README 文件同步更新相同内容及样式 --- README-en.md | 23 +++++++++++++++++++++++ README-zh_CN.md | 22 ++++++++++++++++++++++ README.md | 22 ++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/README-en.md b/README-en.md index 18e3f139..be6442bf 100644 --- a/README-en.md +++ b/README-en.md @@ -8,6 +8,9 @@

Deep Code CLI

+[![][npm-release-shield]][npm-release-link] [![][npm-downloads-shield]][npm-downloads-link] [![][github-contributors-shield]][github-contributors-link] [![][github-forks-shield]][github-forks-link] [![][github-stars-shield]][github-stars-link] +[![][github-issues-shield]][github-issues-link] [![][github-issues-pr-shield]][github-issues-pr-link] [![][github-license-shield]][github-license-link] + English · [中文](./README.md)
@@ -174,3 +177,23 @@ If you find this tool helpful, please consider supporting us by: - Giving us a Star on GitHub (https://github.com/lessweb/deepcode-cli) - Submitting feedback and suggestions - Sharing with your friends and colleagues + + + + +[npm-release-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli +[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square +[npm-downloads-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli +[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE +[github-contributors-link]: https://github.com/lessweb/deepcode-cli/graphs/contributors +[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-forks-link]: https://github.com/lessweb/deepcode-cli/network/members +[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-stars-link]: https://github.com/lessweb/deepcode-cli/network/stargazers +[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-link]: https://github.com/lessweb/deepcode-cli/issues +[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls +[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/master/LICENSE +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file diff --git a/README-zh_CN.md b/README-zh_CN.md index 7b74a502..52f01238 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -8,6 +8,9 @@

Deep Code CLI

+[![][npm-release-shield]][npm-release-link] [![][npm-downloads-shield]][npm-downloads-link] [![][github-contributors-shield]][github-contributors-link] [![][github-forks-shield]][github-forks-link] [![][github-stars-shield]][github-stars-link] +[![][github-issues-shield]][github-issues-link] [![][github-issues-pr-shield]][github-issues-pr-link] [![][github-license-shield]][github-license-link] + [English](README-en.md) · 中文
@@ -173,3 +176,22 @@ npm link - 在 GitHub 上给我们一个 Star (https://github.com/lessweb/deepcode-cli) - 向我们提交反馈和建议 - 分享给你的朋友和同事 + + + +[npm-release-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli +[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square +[npm-downloads-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli +[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE +[github-contributors-link]: https://github.com/lessweb/deepcode-cli/graphs/contributors +[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-forks-link]: https://github.com/lessweb/deepcode-cli/network/members +[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-stars-link]: https://github.com/lessweb/deepcode-cli/network/stargazers +[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-link]: https://github.com/lessweb/deepcode-cli/issues +[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls +[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/master/LICENSE +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file diff --git a/README.md b/README.md index 7b74a502..52f01238 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@

Deep Code CLI

+[![][npm-release-shield]][npm-release-link] [![][npm-downloads-shield]][npm-downloads-link] [![][github-contributors-shield]][github-contributors-link] [![][github-forks-shield]][github-forks-link] [![][github-stars-shield]][github-stars-link] +[![][github-issues-shield]][github-issues-link] [![][github-issues-pr-shield]][github-issues-pr-link] [![][github-license-shield]][github-license-link] + [English](README-en.md) · 中文
@@ -173,3 +176,22 @@ npm link - 在 GitHub 上给我们一个 Star (https://github.com/lessweb/deepcode-cli) - 向我们提交反馈和建议 - 分享给你的朋友和同事 + + + +[npm-release-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli +[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square +[npm-downloads-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli +[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE +[github-contributors-link]: https://github.com/lessweb/deepcode-cli/graphs/contributors +[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-forks-link]: https://github.com/lessweb/deepcode-cli/network/members +[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-stars-link]: https://github.com/lessweb/deepcode-cli/network/stargazers +[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-link]: https://github.com/lessweb/deepcode-cli/issues +[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls +[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/master/LICENSE +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file From b010fb16f92dd01444d683566e41c3ef08c72af7 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 21 May 2026 22:46:17 +0800 Subject: [PATCH 055/212] =?UTF-8?q?docs(readme):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E8=AE=B8=E5=8F=AF=E8=AF=81=E9=93=BE=E6=8E=A5=E5=88=B0=E4=B8=BB?= =?UTF-8?q?=E5=88=86=E6=94=AF=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 README.md 中许可证链接从 master 更改为 main 分支路径 - 同步更新 README-en.md 中的许可证链接路径 - 同步更新 README-zh_CN.md 中的许可证链接路径 - 保持徽章和其他链接不变,确保一致性和正确性 --- README-en.md | 2 +- README-zh_CN.md | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README-en.md b/README-en.md index be6442bf..1e1323d4 100644 --- a/README-en.md +++ b/README-en.md @@ -195,5 +195,5 @@ If you find this tool helpful, please consider supporting us by: [github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls [github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square -[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/master/LICENSE +[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE [github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file diff --git a/README-zh_CN.md b/README-zh_CN.md index 52f01238..29092719 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -193,5 +193,5 @@ npm link [github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls [github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square -[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/master/LICENSE +[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE [github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file diff --git a/README.md b/README.md index 52f01238..29092719 100644 --- a/README.md +++ b/README.md @@ -193,5 +193,5 @@ npm link [github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls [github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square -[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/master/LICENSE +[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE [github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file From c4a2463847d1d294624199d4a066b44b2547df37 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 22 May 2026 08:42:47 +0800 Subject: [PATCH 056/212] feat: update README.md --- README-en.md | 16 ++++++++-------- README-zh_CN.md | 16 ++++++++-------- README.md | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/README-en.md b/README-en.md index 1e1323d4..4bff6afc 100644 --- a/README-en.md +++ b/README-en.md @@ -182,18 +182,18 @@ If you find this tool helpful, please consider supporting us by: [npm-release-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli -[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square +[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square&cacheSeconds=1800 [npm-downloads-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli -[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE +[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE&cacheSeconds=1800 [github-contributors-link]: https://github.com/lessweb/deepcode-cli/graphs/contributors -[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-forks-link]: https://github.com/lessweb/deepcode-cli/network/members -[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-stars-link]: https://github.com/lessweb/deepcode-cli/network/stargazers -[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-issues-link]: https://github.com/lessweb/deepcode-cli/issues -[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls -[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE -[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 \ No newline at end of file diff --git a/README-zh_CN.md b/README-zh_CN.md index 29092719..77db4971 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -180,18 +180,18 @@ npm link [npm-release-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli -[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square +[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square&cacheSeconds=1800 [npm-downloads-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli -[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE +[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE&cacheSeconds=1800 [github-contributors-link]: https://github.com/lessweb/deepcode-cli/graphs/contributors -[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-forks-link]: https://github.com/lessweb/deepcode-cli/network/members -[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-stars-link]: https://github.com/lessweb/deepcode-cli/network/stargazers -[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-issues-link]: https://github.com/lessweb/deepcode-cli/issues -[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls -[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE -[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 \ No newline at end of file diff --git a/README.md b/README.md index 29092719..77db4971 100644 --- a/README.md +++ b/README.md @@ -180,18 +180,18 @@ npm link [npm-release-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli -[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square +[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square&cacheSeconds=1800 [npm-downloads-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli -[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE +[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE&cacheSeconds=1800 [github-contributors-link]: https://github.com/lessweb/deepcode-cli/graphs/contributors -[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-forks-link]: https://github.com/lessweb/deepcode-cli/network/members -[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-stars-link]: https://github.com/lessweb/deepcode-cli/network/stargazers -[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-issues-link]: https://github.com/lessweb/deepcode-cli/issues -[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls -[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square +[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE -[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square \ No newline at end of file +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 \ No newline at end of file From d7d453f55bd11352a38dadb40ebb375cb656e9ba Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 22 May 2026 09:20:55 +0800 Subject: [PATCH 057/212] chore: remove draft doc --- docs/SKILL_new.md | 246 ---------------------------------------------- 1 file changed, 246 deletions(-) delete mode 100644 docs/SKILL_new.md diff --git a/docs/SKILL_new.md b/docs/SKILL_new.md deleted file mode 100644 index 9fc8bd2d..00000000 --- a/docs/SKILL_new.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -name: plan-and-execute -description: Automatically plan and execute requirements. Creates a markdown task list with the UpdatePlan tool, and systematically executes each task while updating progress. Use when working with task planning or when you need to break down and execute complex multi-step requirements. ---- - -# Plan and Execute - -This Skill helps you automatically plan and execute requirements. It creates a structured markdown task list with the UpdatePlan tool and systematically works through each task while keeping progress visible. - -## Quick Start - -When you need to work through a multi-step request: - -1. Analyze the requirements and explore enough project context -2. Clarify unclear or ambiguous requirements with AskUserQuestion -3. Create a markdown task list by calling the UpdatePlan tool -4. Execute tasks one by one, updating the tool plan in real time -5. Revise the remaining plan as new context appears - -## Instructions - -### Step 1: Analyze the requirements - -Identify the requirements from the available context. Explore the project enough to make the plan concrete and accurate. - -If the original requirements are unclear, incomplete, or ambiguous, call the AskUserQuestion tool before creating the task list. Ask only the questions needed to avoid implementing the wrong behavior, and keep each question specific to the decision that affects the plan or acceptance criteria. - -If a required referenced file path is missing, ask for it with AskUserQuestion: - -``` -What is the path to the referenced file? -``` - -Referenced files can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions. If no additional file is needed, continue from the available requirements. - -- What are the main requirements? -- What tasks need to be completed? -- Are there dependencies between tasks? -- What is the complexity level? -- Which files, modules, commands, or tests are relevant? -- What ambiguity would change the implementation or acceptance criteria? - -### Step 2: Create the task list - -Create a structured markdown task list and pass it to the UpdatePlan tool as the `plan` string. The tool input must use this shape: - -```json -{ - "plan": "## Task List\n\n- [ ] Task 1 description\n- [ ] Task 2 description\n- [ ] Task 3 description" -} -``` - -Use this markdown format for the `plan` content: - -```markdown -## Task List - -- [ ] Task 1 description -- [ ] Task 2 description -- [ ] Task 3 description -``` - -Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list. - -### Step 3: Execute tasks systematically - -For each task in the list: - -1. **Refresh the plan**: Before starting the first task and after completing each task, re-evaluate the latest conversation and project context. Update the remaining tasks when scope, order, blockers, or follow-up work changes. -2. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]` -3. **Execute the task**: Use appropriate tools to complete the work -4. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished -5. **Move to next task**: Only ONE task should be in progress at a time - -Important rules: -- Always keep the plan aligned with the latest context before executing the next task -- Always call UpdatePlan BEFORE starting work on a task -- Always call UpdatePlan IMMEDIATELY after completing a task -- Always pass the complete current markdown task list, not a partial diff -- Never work on multiple tasks simultaneously -- Remove tasks that are no longer relevant, and add newly discovered tasks before working on them -- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers - -### Step 4: Handle task breakdown - -If during execution you discover a task is more complex than expected: - -1. Keep the current task as `[>]` -2. Call UpdatePlan with new sub-tasks below it with indentation: - ```markdown - - [>] Main task - - [ ] Sub-task 1 - - [ ] Sub-task 2 - ``` -3. Complete sub-tasks first, then mark the main task as complete with UpdatePlan - -### Step 5: Final verification - -After all tasks are completed (`[x]`): - -1. Review the original requirements to ensure everything is addressed -2. Run any final checks (tests, builds, linting) -3. Call UpdatePlan with every task marked `[x]` -4. Provide a concise completion summary in the final response - -## Task State Symbols - -- `[ ]` - Pending -- `[>]` - In progress -- `[x]` - Completed -- `[!]` - Blocked - -## Examples - -### Example 1: Simple feature request - -**Example requirements:** -```markdown -# 新功能:添加深色模式切换 - -用户应该能够在浅色和深色主题之间切换。 -切换开关应放在设置页面中。 -``` - -**分析后的 UpdatePlan 调用:** -```markdown -## Task List - -- [ ] 在设置页面创建深色模式切换组件 -- [ ] 添加深色模式状态管理(context/store) -- [ ] 实现深色主题的 CSS-in-JS 样式 -- [ ] 更新现有组件以支持主题切换 -- [ ] 运行测试并验证功能 -``` - -**UpdatePlan call during execution:** -```markdown -## Task List - -- [x] 在设置页面创建深色模式切换组件 -- [>] 添加深色模式状态管理(context/store) -- [ ] 实现深色主题的 CSS-in-JS 样式 -- [ ] 更新现有组件以支持主题切换 -- [ ] 运行测试并验证功能 -``` - -### Example 2: Bug fix with investigation - -**Example requirements:** -```markdown -# Fix bug:登录表单提交时崩溃 - -当用户点击提交时,应用崩溃。 -错误信息:"Cannot read property 'email' of undefined" -``` - -**UpdatePlan call after analysis:** -```markdown -## Task List - -- [ ] 在本地复现缺陷 -- [ ] 调查登录表单组件中的错误 -- [ ] 定位 undefined email 属性的根本原因 -- [ ] 实施修复 -- [ ] 添加验证以防止类似问题 -- [ ] 使用各种输入测试修复 -- [ ] 更新错误处理 -``` - -## When to Use This Skill - -Use this Skill when: - -1. **Complex multi-step tasks** - Request requires 3+ distinct steps -2. **Feature implementation** - Building new functionality from requirements -3. **Bug fixing** - Need to investigate, fix, and verify -4. **Refactoring** - Multiple files or components need changes -5. **Detailed requirements** - Specifications need to be translated into concrete tasks -6. **Need progress tracking** - Want visible progress without editing source files - -## When NOT to Use This Skill - -Skip this Skill when: - -1. **Single simple task** - Just one straightforward action needed -2. **Trivial changes** - Quick fixes that don't need planning -3. **Informational requests** - User just wants explanation, not execution -4. **No execution requested** - User only wants brainstorming or a high-level explanation - -## Best Practices - -1. **Be specific with tasks**: "Add login button to navbar" not "Update UI" -2. **Keep tasks atomic**: Each task should be independently completable -3. **Update immediately**: Don't batch status updates, do them in real-time -4. **One task at a time**: Never mark multiple tasks as `[>]` -5. **Handle blockers**: If stuck, create new tasks to resolve the blocker -6. **Verify completion**: Only mark `[x]` when task is fully done - -## Advanced Usage - -### Handling dependencies - -When tasks have dependencies, order them properly: - -```markdown -- [ ] Create database schema -- [ ] Implement API endpoints (depends on schema) -- [ ] Build frontend forms (depends on API) -``` - -### Using sub-tasks - -For complex tasks, break them down: - -```markdown -- [>] Implement authentication system - - [x] Set up JWT library - - [>] Create login endpoint - - [ ] Create logout endpoint - - [ ] Add token refresh logic -``` - -### Adding notes - -Add implementation notes or findings: - -```markdown -- [x] Investigate performance issue - - Note: Found N+1 query in user loader - - Solution: Added dataloader batching -``` - -## Workflow Summary - -1. Analyze the requirements and relevant project context -2. Call AskUserQuestion if the original requirements are unclear or ambiguous -3. Call UpdatePlan with the structured markdown task list -4. Refresh the remaining plan before the first task -5. For each task: - - Update to `[>]` with UpdatePlan - - Execute the task - - Update to `[x]` with UpdatePlan - - Re-evaluate and revise remaining tasks before moving on -6. Call UpdatePlan with all tasks completed and summarize the result - -This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them. From 27b9b7feb444cbeb0b10473216e7f6804530234f Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 22 May 2026 10:22:12 +0800 Subject: [PATCH 058/212] refactor: adjust calling identifyMatchingSkillNames in createSession and replySession --- src/session.ts | 37 +++++++++++++++++++------------------ src/tests/session.test.ts | 10 ++++++++-- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/session.ts b/src/session.ts index 54340e72..3144f883 100644 --- a/src/session.ts +++ b/src/session.ts @@ -901,20 +901,6 @@ The candidate skills are as follows:\n\n`; const signal = controller?.signal; this.throwIfAborted(signal); - if (userPrompt.text) { - const skills = await this.listSkills(); - const skillNames = await this.identifyMatchingSkillNames(skills, userPrompt.text, { signal }); - this.throwIfAborted(signal); - const skillSet = new Set(skillNames); - const matchedSkill = skills.filter((skill) => skillSet.has(skill.name)); - if (Array.isArray(userPrompt.skills)) { - userPrompt.skills.push(...matchedSkill); - } else if (matchedSkill.length > 0) { - userPrompt.skills = matchedSkill; - } - } - userPrompt.skills = await this.normalizeSkills(userPrompt.skills); - this.throwIfAborted(signal); const sessionId = crypto.randomUUID(); this.ensureFileHistorySession(sessionId); const now = new Date().toISOString(); @@ -977,6 +963,21 @@ The candidate skills are as follows:\n\n`; const userMessage = this.buildUserMessage(sessionId, userPrompt); this.appendSessionMessage(sessionId, userMessage); + if (userPrompt.text) { + const skills = await this.listSkills(); + const skillNames = await this.identifyMatchingSkillNames(skills, userPrompt.text, { signal }); + this.throwIfAborted(signal); + const skillSet = new Set(skillNames); + const matchedSkill = skills.filter((skill) => skillSet.has(skill.name)); + if (Array.isArray(userPrompt.skills)) { + userPrompt.skills.push(...matchedSkill); + } else if (matchedSkill.length > 0) { + userPrompt.skills = matchedSkill; + } + } + userPrompt.skills = await this.normalizeSkills(userPrompt.skills); + this.throwIfAborted(signal); + if (userPrompt.skills && userPrompt.skills.length > 0) { for (const skill of userPrompt.skills) { if (skill.isLoaded) { @@ -1022,6 +1023,10 @@ ${skillMd} this.reportNewPrompt(); + this.ensureFileHistorySession(sessionId); + const userMessage = this.buildUserMessage(sessionId, userPrompt); + this.appendSessionMessage(sessionId, userMessage); + if (userPrompt.text) { const skills = await this.listSkills(sessionId); const skillNames = await this.identifyMatchingSkillNames(skills, userPrompt.text, { signal, sessionId }); @@ -1037,10 +1042,6 @@ ${skillMd} userPrompt.skills = await this.normalizeSkills(userPrompt.skills, sessionId); this.throwIfAborted(signal); - this.ensureFileHistorySession(sessionId); - const userMessage = this.buildUserMessage(sessionId, userPrompt); - this.appendSessionMessage(sessionId, userMessage); - if (userPrompt.skills && userPrompt.skills.length > 0) { for (const skill of userPrompt.skills) { if (skill.isLoaded) { diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index fd831990..e5bdcb20 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1952,7 +1952,7 @@ test("SessionManager streams chat completions and counts reasoning progress", as assert.equal(progressEvents[2]?.formattedTokens, "3"); }); -test("SessionManager cancels skill matching before a session is created", async () => { +test("SessionManager persists session and user message before skill matching is cancelled", async () => { const workspace = createTempDir("deepcode-skill-abort-workspace-"); const home = createTempDir("deepcode-skill-abort-home-"); setHomeDir(home); @@ -1981,7 +1981,13 @@ test("SessionManager cancels skill matching before a session is created", async await manager.handleUserPrompt({ text: "please use demo" }); - assert.equal(manager.listSessions().length, 0); + // Session and user message are persisted before skill matching triggers an abort. + assert.equal(manager.listSessions().length, 1); + const [session] = manager.listSessions(); + assert.equal(session?.status, "pending"); + const messages = manager.listSessionMessages(session!.id); + const userMessage = messages.find((m) => m.role === "user"); + assert.equal(userMessage?.content, "please use demo"); }); test("SessionManager treats OpenAI APIUserAbortError as interrupted", async () => { From f1774292a0e2e4420117e1984ce40efd94b38799 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 22 May 2026 11:01:31 +0800 Subject: [PATCH 059/212] feat: implement checkpoints store only explicit Write/Edit file paths --- src/common/file-history.ts | 206 ++++++++++++++++++++++++++++--------- src/tests/session.test.ts | 96 +++++++++-------- 2 files changed, 215 insertions(+), 87 deletions(-) diff --git a/src/common/file-history.ts b/src/common/file-history.ts index d5966d94..2a41d9a1 100644 --- a/src/common/file-history.ts +++ b/src/common/file-history.ts @@ -1,13 +1,26 @@ import * as childProcess from "child_process"; +import * as crypto from "crypto"; import * as fs from "fs"; import * as path from "path"; const FILE_HISTORY_AUTHOR_NAME = "DeepCode Checkpoint"; const FILE_HISTORY_AUTHOR_EMAIL = "deepcode-checkpoint@localhost"; +const MANIFEST_PATH = ".deepcode-file-history.json"; + +type FileHistoryEntry = { + path: string; + blob: string; + mode: "100644"; +}; + +type FileHistoryManifest = { + version: 1; + files: Record; +}; export class GitFileHistory { constructor( - private readonly projectRoot: string, + _projectRoot: string, private readonly gitDir: string ) {} @@ -20,7 +33,7 @@ export class GitFileHistory { try { if (!fs.existsSync(this.gitDir)) { fs.mkdirSync(path.dirname(this.gitDir), { recursive: true }); - this.runGit(["init"], { includeWorkTree: true }); + this.runGit(["init"]); } const current = this.getCurrentCheckpointHash(sessionId); @@ -28,9 +41,9 @@ export class GitFileHistory { return current; } - const emptyTree = this.runGit(["mktree"], { includeWorkTree: false, input: "" }).trim(); - const commitHash = this.createCommit(emptyTree, null, "Initial checkpoint"); - this.runGit(["update-ref", branchRef, commitHash], { includeWorkTree: false }); + const treeHash = this.createTree(emptyManifest()); + const commitHash = this.createCommit(treeHash, null, "Initial checkpoint"); + this.runGit(["update-ref", branchRef, commitHash]); return commitHash; } catch { return undefined; @@ -44,9 +57,7 @@ export class GitFileHistory { } try { - const hash = this.runGit(["rev-parse", "--verify", `${branchRef}^{commit}`], { - includeWorkTree: false, - }).trim(); + const hash = this.runGit(["rev-parse", "--verify", `${branchRef}^{commit}`]).trim(); return isCommitHash(hash) ? hash : undefined; } catch { return undefined; @@ -59,10 +70,8 @@ export class GitFileHistory { return undefined; } - const relativePaths = filePaths - .map((filePath) => this.toProjectRelativeGitPath(filePath)) - .filter((filePath): filePath is string => Boolean(filePath)); - if (relativePaths.length === 0) { + const absolutePaths = uniqueAbsolutePaths(filePaths); + if (absolutePaths.length === 0) { return this.getCurrentCheckpointHash(sessionId); } @@ -71,18 +80,30 @@ export class GitFileHistory { if (!parentHash) { return undefined; } - this.runGit(["read-tree", "--reset", branchRef], { includeWorkTree: true }); - this.runGit(["add", "-f", "-A", "--", ...relativePaths], { includeWorkTree: true }); - const treeHash = this.runGit(["write-tree"], { includeWorkTree: false }).trim(); - const parentTreeHash = this.runGit(["rev-parse", `${parentHash}^{tree}`], { - includeWorkTree: false, - }).trim(); + + const manifest = this.readManifest(parentHash); + for (const filePath of absolutePaths) { + const key = this.getFileKey(filePath); + if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + delete manifest.files[key]; + continue; + } + + manifest.files[key] = { + path: filePath, + blob: this.hashFile(filePath), + mode: "100644", + }; + } + + const treeHash = this.createTree(manifest); + const parentTreeHash = this.runGit(["rev-parse", `${parentHash}^{tree}`]).trim(); if (treeHash === parentTreeHash) { return parentHash; } const commitHash = this.createCommit(treeHash, parentHash, message); - this.runGit(["update-ref", branchRef, commitHash, parentHash], { includeWorkTree: false }); + this.runGit(["update-ref", branchRef, commitHash, parentHash]); return commitHash; } catch { return undefined; @@ -101,7 +122,8 @@ export class GitFileHistory { } try { - this.runGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false }); + this.runGit(["cat-file", "-e", `${checkpointHash}^{commit}`]); + this.readManifest(checkpointHash); return true; } catch { return false; @@ -116,16 +138,24 @@ export class GitFileHistory { if (!branchRef || !fs.existsSync(this.gitDir)) { throw new Error("File history Git repository was not found for this project."); } - this.runGit(["cat-file", "-e", `${checkpointHash}^{commit}`], { includeWorkTree: false }); + this.runGit(["cat-file", "-e", `${checkpointHash}^{commit}`]); - try { - this.runGit(["read-tree", "--reset", branchRef], { includeWorkTree: true }); - } catch { - // If the session branch is missing, fall back to the target tree only. - // The target checkpoint has already been validated above. + const currentHash = this.getCurrentCheckpointHash(sessionId); + const currentManifest = currentHash ? this.readManifest(currentHash) : emptyManifest(); + const targetManifest = this.readManifest(checkpointHash); + + for (const [key, entry] of Object.entries(currentManifest.files)) { + if (!targetManifest.files[key]) { + removeTrackedFile(entry.path); + } } - this.runGit(["read-tree", "--reset", "-u", checkpointHash], { includeWorkTree: true }); - this.runGit(["update-ref", branchRef, checkpointHash], { includeWorkTree: false }); + + for (const entry of Object.values(targetManifest.files)) { + fs.mkdirSync(path.dirname(entry.path), { recursive: true }); + fs.writeFileSync(entry.path, this.readBlob(entry.blob)); + } + + this.runGit(["update-ref", branchRef, checkpointHash]); } private getSessionBranchRef(sessionId: string): string | null { @@ -142,41 +172,125 @@ export class GitFileHistory { } args.push("-m", message); return this.runGit(args, { - includeWorkTree: false, env: getFileHistoryGitEnv(), }).trim(); } - private toProjectRelativeGitPath(filePath: string): string | null { - const absolutePath = path.resolve(filePath); - const relativePath = path.relative(this.projectRoot, absolutePath); - if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) { - return null; + private createTree(manifest: FileHistoryManifest): string { + const normalizedManifest = normalizeManifest(manifest); + const manifestBlob = this.hashContent(`${JSON.stringify(normalizedManifest, null, 2)}\n`); + const entries: string[] = [`100644 blob ${manifestBlob}\t${MANIFEST_PATH}\0`]; + + for (const [key, entry] of Object.entries(normalizedManifest.files)) { + entries.push(`${entry.mode} blob ${entry.blob}\t${key}\0`); } - return relativePath.split(path.sep).join("/"); + + return this.runGit(["mktree", "-z"], { input: entries.join("") }).trim(); } - private runGit( - args: string[], - options: { includeWorkTree: boolean; input?: string; env?: NodeJS.ProcessEnv } - ): string { - const gitArgs = ["-c", "core.autocrlf=false", "-c", "core.eol=lf", `--git-dir=${this.gitDir}`]; - if (options.includeWorkTree) { - gitArgs.push(`--work-tree=${this.projectRoot}`); + private readManifest(commitHash: string): FileHistoryManifest { + const buffer = this.runGitBuffer(["cat-file", "blob", `${commitHash}:${MANIFEST_PATH}`]); + const parsed = JSON.parse(buffer.toString("utf8")) as FileHistoryManifest; + if (!parsed || parsed.version !== 1 || !parsed.files || typeof parsed.files !== "object") { + throw new Error("Invalid file history manifest."); + } + return normalizeManifest(parsed); + } + + private readBlob(blobHash: string): Buffer { + if (!isCommitHash(blobHash)) { + throw new Error("Invalid file history blob hash."); + } + return this.runGitBuffer(["cat-file", "blob", blobHash]); + } + + private hashFile(filePath: string): string { + const blobHash = this.runGit(["hash-object", "-w", "--", filePath]).trim(); + if (!isCommitHash(blobHash)) { + throw new Error("Invalid file history blob hash."); } - gitArgs.push(...args); + return blobHash; + } + + private hashContent(content: string): string { + const blobHash = this.runGit(["hash-object", "-w", "--stdin"], { input: content }).trim(); + if (!isCommitHash(blobHash)) { + throw new Error("Invalid file history blob hash."); + } + return blobHash; + } + + private getFileKey(filePath: string): string { + const hash = crypto.createHash("sha256").update(filePath).digest("hex"); + return `files-${hash}`; + } + + private runGit(args: string[], options: { input?: string | Buffer; env?: NodeJS.ProcessEnv } = {}): string { + return this.spawnGit(args, options, "utf8") as string; + } + + private runGitBuffer(args: string[], options: { input?: string | Buffer; env?: NodeJS.ProcessEnv } = {}): Buffer { + return this.spawnGit(args, options, "buffer") as Buffer; + } + + private spawnGit( + args: string[], + options: { input?: string | Buffer; env?: NodeJS.ProcessEnv }, + encoding: BufferEncoding | "buffer" + ): string | Buffer { + const gitArgs = ["-c", "core.autocrlf=false", "-c", "core.eol=lf", `--git-dir=${this.gitDir}`, ...args]; const result = childProcess.spawnSync("git", gitArgs, { - encoding: "utf8", + encoding, input: options.input, env: options.env, stdio: ["pipe", "pipe", "pipe"], }); if (result.status !== 0) { - const detail = (result.stderr || result.stdout || "").trim(); + const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8") : result.stderr; + const stdout = Buffer.isBuffer(result.stdout) ? result.stdout.toString("utf8") : result.stdout; + const detail = (stderr || stdout || "").trim(); throw new Error(detail || `git ${args.join(" ")} failed`); } - return result.stdout ?? ""; + return result.stdout ?? (encoding === "buffer" ? Buffer.alloc(0) : ""); + } +} + +function emptyManifest(): FileHistoryManifest { + return { version: 1, files: {} }; +} + +function normalizeManifest(manifest: FileHistoryManifest): FileHistoryManifest { + const files: Record = {}; + for (const [key, entry] of Object.entries(manifest.files).sort(([left], [right]) => left.localeCompare(right))) { + if (!isValidStoredPath(key) || !entry || entry.mode !== "100644" || !isCommitHash(entry.blob)) { + throw new Error("Invalid file history manifest."); + } + files[key] = { + path: path.resolve(entry.path), + blob: entry.blob, + mode: "100644", + }; + } + return { version: 1, files }; +} + +function uniqueAbsolutePaths(filePaths: string[]): string[] { + return Array.from(new Set(filePaths.map((filePath) => path.resolve(filePath)))); +} + +function isValidStoredPath(value: string): boolean { + return /^files-[0-9a-f]{64}$/.test(value); +} + +function removeTrackedFile(filePath: string): void { + if (!fs.existsSync(filePath)) { + return; + } + const stat = fs.lstatSync(filePath); + if (stat.isDirectory()) { + return; } + fs.unlinkSync(filePath); } function getFileHistoryGitEnv(): NodeJS.ProcessEnv { diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index e5bdcb20..08d61e96 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -4,6 +4,7 @@ import { execFileSync } from "node:child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; +import { GitFileHistory } from "../common/file-history"; import { SessionManager, type SessionMessage } from "../session"; const originalFetch = globalThis.fetch; @@ -1040,6 +1041,54 @@ test("Write tool advances file-history while preserving the user prompt checkpoi assert.equal(fs.existsSync(filePath), false); }); +test("Write checkpoints restore tool-touched files outside the workspace and leave unrelated files alone", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-write-outside-workspace-"); + const outsideDir = createTempDir("deepcode-write-outside-target-"); + const home = createTempDir("deepcode-write-outside-home-"); + setHomeDir(home); + + const outsideFilePath = path.join(outsideDir, "outside.txt"); + const unrelatedWorkspaceFilePath = path.join(workspace, "unrelated.txt"); + const manager = createMockedClientSessionManager(workspace, [ + { + choices: [ + { + message: { + content: "", + tool_calls: [ + { + id: "call-write-outside", + type: "function", + function: { + name: "write", + arguments: JSON.stringify({ file_path: outsideFilePath, content: "outside\n" }), + }, + }, + ], + }, + }, + ], + }, + createChatResponse("done", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }), + ]); + + const sessionId = await manager.createSession({ text: "create an outside file" }); + const userMessage = manager.listSessionMessages(sessionId).find((message) => message.role === "user"); + assert.ok(userMessage?.checkpointHash); + assert.equal(fs.readFileSync(outsideFilePath, "utf8"), "outside\n"); + + fs.writeFileSync(unrelatedWorkspaceFilePath, "keep\n", "utf8"); + manager.restoreSessionCode(sessionId, userMessage.id); + + assert.equal(fs.existsSync(outsideFilePath), false); + assert.equal(fs.readFileSync(unrelatedWorkspaceFilePath, "utf8"), "keep\n"); +}); + test("missing git executable does not block sessions or Write tool calls", async () => { const workspace = createTempDir("deepcode-no-git-write-workspace-"); const home = createTempDir("deepcode-no-git-write-home-"); @@ -2146,43 +2195,18 @@ function createFileHistoryCommit( ): string { const projectCode = workspace.replace(/[\\/]/g, "-").replace(/:/g, ""); const gitDir = path.join(home, ".deepcode", "projects", projectCode, "file-history", ".git"); - const branchRef = `refs/heads/${sessionId}`; - fs.mkdirSync(path.dirname(gitDir), { recursive: true }); - if (!fs.existsSync(gitDir)) { - runFileHistoryGit(gitDir, workspace, ["init"]); - } - - let parentHash = ""; - try { - parentHash = runFileHistoryGit(gitDir, workspace, ["rev-parse", "--verify", `${branchRef}^{commit}`]).trim(); - } catch { - const emptyTree = runFileHistoryGit(gitDir, workspace, ["mktree"], ""); - parentHash = runFileHistoryGit( - gitDir, - workspace, - ["commit-tree", emptyTree.trim(), "-m", "initial checkpoint"], - "", - fileHistoryCommitEnv() - ).trim(); - runFileHistoryGit(gitDir, workspace, ["update-ref", branchRef, parentHash]); - } - runFileHistoryGit(gitDir, workspace, ["read-tree", "--reset", branchRef]); + const fileHistory = new GitFileHistory(workspace, gitDir); + fileHistory.ensureSession(sessionId); + const filePaths: string[] = []; for (const [relativePath, content] of Object.entries(files)) { const filePath = path.join(workspace, relativePath); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content, "utf8"); + filePaths.push(filePath); } - runFileHistoryGit(gitDir, workspace, ["add", "-f", "-A", "--", ...Object.keys(files)]); - const treeHash = runFileHistoryGit(gitDir, workspace, ["write-tree"]).trim(); - const commitHash = runFileHistoryGit( - gitDir, - workspace, - ["commit-tree", treeHash, "-p", parentHash, "-m", "checkpoint"], - "", - fileHistoryCommitEnv() - ).trim(); - runFileHistoryGit(gitDir, workspace, ["update-ref", branchRef, commitHash, parentHash]); + const commitHash = fileHistory.recordCheckpoint(sessionId, filePaths, "checkpoint"); + assert.ok(commitHash); return commitHash; } @@ -2205,16 +2229,6 @@ function runFileHistoryGit( ); } -function fileHistoryCommitEnv(): NodeJS.ProcessEnv { - return { - ...process.env, - GIT_AUTHOR_NAME: "DeepCode Test", - GIT_AUTHOR_EMAIL: "deepcode-test@example.com", - GIT_COMMITTER_NAME: "DeepCode Test", - GIT_COMMITTER_EMAIL: "deepcode-test@example.com", - }; -} - function createSessionManager(projectRoot: string, machineId: string): SessionManager { return new SessionManager({ projectRoot, From 683a51106b1d9faa8d58811da80f5857eb641934 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 22 May 2026 21:41:14 +0800 Subject: [PATCH 060/212] feat: implement the permission system --- docs/issue_0522.md | 241 +++++++++++++ src/common/permissions.ts | 464 ++++++++++++++++++++++++++ src/prompt.ts | 23 +- src/session.ts | 229 +++++++++++-- src/settings.ts | 97 ++++++ src/tests/permissions.test.ts | 120 +++++++ src/tests/prompt.test.ts | 13 + src/tests/session.test.ts | 192 +++++++++++ src/tests/settings-and-notify.test.ts | 29 ++ src/ui/App.tsx | 74 +++- src/ui/PermissionPrompt.tsx | 229 +++++++++++++ src/ui/PromptInput.tsx | 4 +- templates/tools/bash.md | 28 +- 13 files changed, 1719 insertions(+), 24 deletions(-) create mode 100644 docs/issue_0522.md create mode 100644 src/common/permissions.ts create mode 100644 src/tests/permissions.test.ts create mode 100644 src/ui/PermissionPrompt.tsx diff --git a/docs/issue_0522.md b/docs/issue_0522.md new file mode 100644 index 00000000..2e9fd1a1 --- /dev/null +++ b/docs/issue_0522.md @@ -0,0 +1,241 @@ +# Deep Code Permission System (设计文档) + +scopes是枚举值,列表如下: + +``` +# PermissionScope +read-in-cwd +read-out-cwd +write-in-cwd +write-out-cwd +delete-in-cwd +delete-out-cwd +query-git-log +mutate-git-log +network +mcp +``` + +settings.json的配置项(例子): + +``` +{ + "permissions": { + "allow": [ + "write-in-cwd" + ], + "deny": [ + "write-out-cwd" + ], + "ask": [ + "read-out-cwd" + ], + "defaultMode": "allowAll|askAll" // 默认是allowAll + } +} +``` + +工具和PermissionScope可能的对应关系: + +- read: read-in-cwd, read-out-cwd +- write: write-in-cwd, write-out-cwd +- edit: write-in-cwd, write-out-cwd +- WebSearch: network +- mcp__*: mcp +- bash: 每一次bash命令需要的scope在sideEffects字段中。如果sideEffects字段为undefined|null,或者sideEffects包含了"unknown"则总是ask +- 其他: 无权限要求,总是允许 + +## bash tool的参数schema新增sideEffects字段 + +目标:让LLM在每一次调用`bash`时显式声明该命令可能需要的权限范围,后端只信任这个结构化字段,不从自然语言`description`中推断权限。 + +需要同步修改两处schema: + +1. `src/prompt.ts`里的`getTools()`内置`bash`工具定义。 +2. `templates/tools/bash.md`里的`bash`工具说明和JSON schema示例。 + +新增字段: + +``` +sideEffects: PermissionScope[] | ["unknown"] +``` + +`bash`可声明的scope只包含文件系统、Git历史和网络权限,不包含`mcp`: + +``` +read-in-cwd +read-out-cwd +write-in-cwd +write-out-cwd +delete-in-cwd +delete-out-cwd +query-git-log +mutate-git-log +network +unknown +``` + +建议schema如下: + +```json +{ + "type": "object", + "properties": { + "command": { + "description": "The command to execute", + "type": "string" + }, + "description": { + "description": "Clear, concise description of what this command does in active voice.", + "type": "string" + }, + "sideEffects": { + "description": "Permission scopes required by this bash command. Use [] only for commands that do not read, write, delete, or access the network. Use [\"unknown\"] when the effects cannot be classified safely.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "unknown" + ] + }, + "uniqueItems": true + } + }, + "required": [ + "command", + "sideEffects" + ], + "additionalProperties": false +} +``` + +字段语义: + +- `sideEffects: []`表示命令不需要权限,例如`date`、`node --version`这类只读取进程环境或输出版本信息的命令。 +- `sideEffects`必须按最小必要权限填写;例如`rg foo src`是`["read-in-cwd"]`,`npm install`通常是`["write-in-cwd", "network"]`。 +- 如果命令访问项目目录之外的路径,需要使用`*-out-cwd`;例如`cat /etc/hosts`是`["read-out-cwd"]`。 +- 删除类操作使用`delete-*`;如果同一条命令还会写入其他文件,再同时声明对应的`write-*`。 +- 查询Git历史使用`query-git-log`;例如`git log`、`git show HEAD`、`git blame`、`git diff HEAD~1..HEAD`这类读取提交历史、提交对象或历史diff的命令。 +- 修改Git历史或引用使用`mutate-git-log`;例如`git commit`、`git reset`、`git rebase`、`git merge`、`git cherry-pick`、`git tag`这类会创建提交、移动引用或改写提交图的命令。 +- Git命令如果同时读写工作区文件,也需要同时声明文件系统scope;例如`git checkout -- src/foo.ts`需要`["write-in-cwd"]`,`git reset --hard HEAD~1`需要`["write-in-cwd", "mutate-git-log"]`。 +- `unknown`只能单独出现为`["unknown"]`,不能和其他scope混用。 + +示例: + +```json +{ "command": "date", "description": "Show current date", "sideEffects": [] } +{ "command": "rg \"TODO\" src", "description": "Search TODO markers in source files", "sideEffects": ["read-in-cwd"] } +{ "command": "npm install", "description": "Install package dependencies", "sideEffects": ["write-in-cwd", "network"] } +{ "command": "rm -rf dist", "description": "Delete build output directory", "sideEffects": ["delete-in-cwd"] } +{ "command": "curl -s https://example.com", "description": "Fetch example.com response", "sideEffects": ["network"] } +{ "command": "git show --stat HEAD", "description": "Show file statistics for HEAD", "sideEffects": ["query-git-log"] } +{ "command": "git blame src/prompt.ts", "description": "Show line authorship for prompt source", "sideEffects": ["read-in-cwd", "query-git-log"] } +{ "command": "git reset --hard HEAD~1", "description": "Reset branch and worktree to previous commit", "sideEffects": ["write-in-cwd", "mutate-git-log"] } +``` + +## 核心数据结构设计 + +``` +export type UserPromptContent = { + text?: string; + imageUrls?: string[]; + skills?: SkillInfo[]; ++ permissions?: [{toolCallId: "...", permission: "allow|deny"}]; ++ alwaysAllows?: [""]; +}; + +export type SessionEntry = { + id: string; + ... + toolCalls: unknown[] | null; // 例如:[{"id":"...","function":{"name":"bash","arguments":"{\"command\": \"...\", \"description\": \"...\"}"}}] + status: SessionStatus; ++ askPermissions?: [{toolCallId: "...", scopes: [""], name: "...", command: "...", description?: "..."}]; +}; + +export type SessionStatus = "... | "completed" | "interrupted" | "ask_permission"; // 新增 ask_permission 状态 + +export type SessionMessage = { + ... + meta?: MessageMeta; + ... +}; + +export type MessageMeta = { + ... ++ permissions?: [{toolCallId: "...", permission: "allow|deny|ask"}]; ++ userPrompt?: UserPromptContent; //对于role为user的消息,持久化userPrompt可方便后续排查问题 +}; +``` + +## 前端流程 + +如果当前会话状态不是ask_permission,则保持现状。会话状态是ask_permission时: + +对SessionEntry.askPermissions中每一个toolCallId的每一个scope,显示权限弹窗(示例): + +``` + + + + + + Do you want to proceed? + ❯ 1. Yes + 2. Yes, and always allow + 3. No +``` + +注意对于read/write/edit的``,格式可以是"工具名称+相对或绝对文件路径",例如:`read ~/dev/main.c` + +如果在权限弹窗过程中,用户按Esc,则走现有的interrupt流程(会话状态也应该变成"interrupted")。 + +提醒注意一种情况:例如askPermissions里面有好几个item的scopes是`["write-in-cwd"]`,如果用户已经在第一个权限弹窗选了"always allow write in CWD `~/dev/qrcode_test/`",则后面的几个scopes是`["write-in-cwd"]`的item就不用显示权限弹窗了。 + +如果用户完成了所有权限弹窗的选择,则判断: + +1. 如果用户提交的结果中包含deny,则需要用户输入user prompt,按回车手动提交replySession()。 + - 如果用户没有输入user prompt就退出了,或者切换到了其他会话。则重新开始这个会话时,由于会话状态还是ask_permission,则会重新显示权限弹窗,要求用户选择。 +2. 如果用户提交的结果中不包含deny,则以`/continue`作为UserPromptContent.text内容,前端自动提交replySession()。 + + +## 后端流程 + +后端主要是对replySession()和activateSession()进行升级: + +1. 支持传入UserPromptContent.permissions和alwaysAllows +2. 如果UserPromptContent.alwaysAllows非空,将其中的scopes追加写入项目级别的settings.json配置文件(`permissions.allow`字段),避免重复写入已存在的项。 +3. 检查当前会话消息列表末尾是否存在连续的role为assistant的有tool_calls的消息,也就是"待执行消息"。如果没有,则走现有流程。 +4. 对于每一条待执行消息,先检查UserPromptContent.permissions中对应的toolCallId的用户授权是allow还是deny + - 如果是allow,则正常执行这个toolCall + - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限。例如: + ``` + { + "ok": false, + "name": "edit", + "error": "用户已禁用了在项目目录之外修改文件的权限,请不要尝试用任何方式修改目录之外的文件" + } + ``` +5. 如果对于某条待执行消息,在UserPromptContent.permissions没有出现对应的toolCallId的用户授权,则检查它的 SessionMessage.meta.permissions[].permission 是allow还是deny还是ask + - 如果是allow,则正常执行这个toolCall + - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限 + - 如果是ask,则直接返回失败结果,报错信息提示LLM用户未授权执行。例如: + ``` + { + "ok": false, + "name": "edit", + "error": "用户暂未授权执行,如果有必要,可重新尝试执行" + } + ``` + - 如果不存在,则正常执行这个toolCall(兼容老版本会话数据) +6. 当LLM返回了新的待执行消息时,不要立即执行,而是: + 1. 根据配置的permissions和defaultMode,计算出SessionMessage.meta.permissions字段 + 2. 如果存在一个待执行消息的SessionMessage.meta.permissions[].permission是ask,则把SessionEntry.status设置为"ask_permission",并设置好SessionEntry.askPermissions,然后退出activateSession,这样就回到了上面的前端流程。 diff --git a/src/common/permissions.ts b/src/common/permissions.ts new file mode 100644 index 00000000..e9aae01c --- /dev/null +++ b/src/common/permissions.ts @@ -0,0 +1,464 @@ +import * as fs from "fs"; +import * as path from "path"; +import type { DeepcodingSettings, PermissionScope, PermissionSettings } from "../settings"; +import { isAbsoluteFilePath, normalizeFilePath } from "./state"; + +export type BashPermissionScope = Exclude | "unknown"; + +export type PermissionDecision = "allow" | "deny" | "ask"; + +export type UserToolPermission = { + toolCallId: string; + permission: "allow" | "deny"; +}; + +export type MessageToolPermission = { + toolCallId: string; + permission: PermissionDecision; +}; + +export type AskPermissionScope = PermissionScope | "unknown"; + +export type AskPermissionRequest = { + toolCallId: string; + scopes: AskPermissionScope[]; + name: string; + command: string; + description?: string; +}; + +export type PermissionToolCall = { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +}; + +export type PermissionToolExecution = { + toolCallId: string; + content: string; + result: { + ok: boolean; + name: string; + output?: string; + error?: string; + metadata?: Record; + awaitUserResponse?: boolean; + followUpMessages?: Array<{ role: "system"; content: string; contentParams?: unknown | null }>; + }; +}; + +export type PermissionPlan = { + permissions: MessageToolPermission[]; + askPermissions: AskPermissionRequest[]; +}; + +export type ComputeToolCallPermissionsOptions = { + sessionId: string; + projectRoot: string; + toolCalls: unknown[]; + settings?: Required; + resolveSnippetPath?: (sessionId: string, snippetId: string) => string | null | undefined; +}; + +export function parseToolCallForPermissions(toolCall: unknown): PermissionToolCall | null { + if (!toolCall || typeof toolCall !== "object") { + return null; + } + const record = toolCall as { + id?: unknown; + type?: unknown; + function?: { name?: unknown; arguments?: unknown }; + }; + if (typeof record.id !== "string" || !record.function || typeof record.function !== "object") { + return null; + } + if (typeof record.function.name !== "string") { + return null; + } + return { + id: record.id, + type: "function", + function: { + name: record.function.name, + arguments: typeof record.function.arguments === "string" ? record.function.arguments : "", + }, + }; +} + +export function buildPermissionToolExecution( + toolCall: PermissionToolCall, + options: { + permissionOverrides?: UserToolPermission[]; + messagePermissions?: MessageToolPermission[]; + } +): PermissionToolExecution | null { + const permission = resolveToolCallPermission(toolCall.id, options); + if (permission === "allow") { + return null; + } + if (permission === "deny") { + return buildSyntheticToolExecution( + toolCall, + "User denied the required permission for this tool call. Do not try to bypass this decision." + ); + } + return buildSyntheticToolExecution( + toolCall, + "The user has not authorized this tool call yet. Retry only if the permission is still necessary." + ); +} + +export function resolveToolCallPermission( + toolCallId: string, + options: { + permissionOverrides?: UserToolPermission[]; + messagePermissions?: MessageToolPermission[]; + } +): PermissionDecision { + const override = options.permissionOverrides?.find((item) => item.toolCallId === toolCallId); + if (override?.permission === "allow" || override?.permission === "deny") { + return override.permission; + } + const messagePermission = options.messagePermissions?.find((item) => item.toolCallId === toolCallId); + if ( + messagePermission?.permission === "allow" || + messagePermission?.permission === "deny" || + messagePermission?.permission === "ask" + ) { + return messagePermission.permission; + } + return "allow"; +} + +export function buildSyntheticToolExecution(toolCall: PermissionToolCall, error: string): PermissionToolExecution { + const result = { + ok: false, + name: toolCall.function.name, + error, + }; + return { + toolCallId: toolCall.id, + content: JSON.stringify(result, null, 2), + result, + }; +} + +export function computeToolCallPermissions(options: ComputeToolCallPermissionsOptions): PermissionPlan { + const permissions: MessageToolPermission[] = []; + const askPermissions: AskPermissionRequest[] = []; + + for (const rawToolCall of options.toolCalls) { + const toolCall = parseToolCallForPermissions(rawToolCall); + if (!toolCall) { + continue; + } + const request = describeToolPermissionRequest({ + sessionId: options.sessionId, + projectRoot: options.projectRoot, + toolCall, + resolveSnippetPath: options.resolveSnippetPath, + }); + const permission = evaluatePermissionScopes(request.scopes, options.settings); + permissions.push({ toolCallId: toolCall.id, permission }); + if (permission === "ask") { + askPermissions.push({ + toolCallId: toolCall.id, + scopes: request.scopes, + name: request.name, + command: request.command, + description: request.description, + }); + } + } + + return { permissions, askPermissions }; +} + +export function describeToolPermissionRequest(options: { + sessionId: string; + projectRoot: string; + toolCall: PermissionToolCall; + resolveSnippetPath?: (sessionId: string, snippetId: string) => string | null | undefined; +}): AskPermissionRequest { + const name = options.toolCall.function.name; + const args = parseToolArgumentsForPermissions(options.toolCall.function.arguments); + + if (name === "read" || name === "Read") { + const filePath = typeof args.file_path === "string" ? args.file_path : ""; + return { + toolCallId: options.toolCall.id, + name, + command: formatToolPathCommand("read", filePath), + scopes: filePath ? [isPathInProject(options.projectRoot, filePath) ? "read-in-cwd" : "read-out-cwd"] : [], + }; + } + + if (name === "write" || name === "Write") { + const filePath = typeof args.file_path === "string" ? args.file_path : ""; + return { + toolCallId: options.toolCall.id, + name, + command: formatToolPathCommand("write", filePath), + scopes: filePath ? [isPathInProject(options.projectRoot, filePath) ? "write-in-cwd" : "write-out-cwd"] : [], + }; + } + + if (name === "edit" || name === "Edit") { + const filePath = resolveEditPermissionPath(options.sessionId, args, options.resolveSnippetPath); + return { + toolCallId: options.toolCall.id, + name, + command: formatToolPathCommand("edit", filePath), + scopes: filePath + ? [isPathInProject(options.projectRoot, filePath) ? "write-in-cwd" : "write-out-cwd"] + : ["write-out-cwd"], + }; + } + + if (name === "bash" || name === "Bash") { + const command = typeof args.command === "string" ? args.command : "bash"; + const description = typeof args.description === "string" ? args.description : undefined; + return { + toolCallId: options.toolCall.id, + name: "bash", + command, + description, + scopes: parseBashSideEffects(args.sideEffects), + }; + } + + if (name === "WebSearch") { + const query = typeof args.query === "string" ? args.query : "WebSearch"; + return { + toolCallId: options.toolCall.id, + name, + command: query, + scopes: ["network"], + }; + } + + if (name.startsWith("mcp__")) { + return { + toolCallId: options.toolCall.id, + name, + command: name, + scopes: ["mcp"], + }; + } + + return { + toolCallId: options.toolCall.id, + name, + command: name, + scopes: [], + }; +} + +export function evaluatePermissionScopes( + scopes: AskPermissionScope[], + settings: Required = { + allow: [], + deny: [], + ask: [], + defaultMode: "allowAll", + } +): PermissionDecision { + if (scopes.includes("unknown")) { + return "ask"; + } + if (scopes.length === 0) { + return "allow"; + } + const permissionScopes = scopes.filter((scope): scope is PermissionScope => scope !== "unknown"); + if (permissionScopes.some((scope) => settings.deny.includes(scope))) { + return "deny"; + } + if (permissionScopes.some((scope) => settings.ask.includes(scope))) { + return "ask"; + } + if (permissionScopes.every((scope) => settings.allow.includes(scope))) { + return "allow"; + } + return settings.defaultMode === "askAll" ? "ask" : "allow"; +} + +export function parseBashSideEffects(value: unknown): AskPermissionScope[] { + const validScopes = new Set([ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "unknown", + ]); + if (!Array.isArray(value)) { + return ["unknown"]; + } + const scopes: AskPermissionScope[] = []; + for (const item of value) { + if (typeof item !== "string" || !validScopes.has(item as AskPermissionScope)) { + return ["unknown"]; + } + const scope = item as AskPermissionScope; + if (!scopes.includes(scope)) { + scopes.push(scope); + } + } + if (scopes.includes("unknown")) { + return ["unknown"]; + } + return scopes; +} + +export function parseToolArgumentsForPermissions(rawArguments: string): Record { + if (!rawArguments) { + return {}; + } + try { + const parsed = JSON.parse(rawArguments); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : {}; + } catch { + return {}; + } +} + +export function resolveEditPermissionPath( + sessionId: string, + args: Record, + resolveSnippetPath?: (sessionId: string, snippetId: string) => string | null | undefined +): string { + const filePath = typeof args.file_path === "string" ? args.file_path : ""; + if (filePath) { + return filePath; + } + const snippetId = typeof args.snippet_id === "string" ? args.snippet_id : ""; + return snippetId ? (resolveSnippetPath?.(sessionId, snippetId) ?? "") : ""; +} + +export function formatToolPathCommand(toolName: string, filePath: string): string { + return filePath ? `${toolName} ${filePath}` : toolName; +} + +export function isPathInProject(projectRoot: string, filePath: string): boolean { + const normalized = normalizeFilePath(filePath); + const absolutePath = isAbsoluteFilePath(normalized) ? normalized : path.resolve(projectRoot, normalized); + const relative = path.relative(path.resolve(projectRoot), path.resolve(absolutePath)); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +export function hasUserPermissionReplies(value: { permissions?: unknown; alwaysAllows?: unknown }): boolean { + return Boolean( + (Array.isArray(value.permissions) && value.permissions.length > 0) || + (Array.isArray(value.alwaysAllows) && value.alwaysAllows.length > 0) + ); +} + +export function appendProjectPermissionAllows(projectRoot: string, scopes: PermissionScope[] | undefined): void { + if (!Array.isArray(scopes) || scopes.length === 0) { + return; + } + const validScopes = new Set([ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "mcp", + ]); + const nextScopes = scopes.filter((scope) => validScopes.has(scope)); + if (nextScopes.length === 0) { + return; + } + const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); + let settings: DeepcodingSettings = {}; + try { + if (fs.existsSync(settingsPath)) { + const parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + settings = parsed as DeepcodingSettings; + } + } + } catch { + settings = {}; + } + const currentAllow = Array.isArray(settings.permissions?.allow) ? settings.permissions.allow : []; + const allow = [...currentAllow]; + for (const scope of nextScopes) { + if (!allow.includes(scope)) { + allow.push(scope); + } + } + if (allow.length === currentAllow.length) { + return; + } + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + fs.writeFileSync( + settingsPath, + `${JSON.stringify( + { + ...settings, + permissions: { + ...(settings.permissions ?? {}), + allow, + }, + }, + null, + 2 + )}\n`, + "utf8" + ); +} + +export function normalizeAskPermissions(value: unknown): AskPermissionRequest[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + const result: AskPermissionRequest[] = []; + for (const item of value) { + if (!item || typeof item !== "object") { + continue; + } + const record = item as Record; + if (typeof record.toolCallId !== "string" || typeof record.name !== "string") { + continue; + } + const scopes = Array.isArray(record.scopes) + ? record.scopes.filter((scope): scope is AskPermissionScope => isAskPermissionScope(scope)) + : []; + result.push({ + toolCallId: record.toolCallId, + scopes, + name: record.name, + command: typeof record.command === "string" ? record.command : record.name, + description: typeof record.description === "string" ? record.description : undefined, + }); + } + return result.length > 0 ? result : undefined; +} + +export function isAskPermissionScope(value: unknown): value is AskPermissionScope { + return ( + value === "read-in-cwd" || + value === "read-out-cwd" || + value === "write-in-cwd" || + value === "write-out-cwd" || + value === "delete-in-cwd" || + value === "delete-out-cwd" || + value === "query-git-log" || + value === "mutate-git-log" || + value === "network" || + value === "mcp" || + value === "unknown" + ); +} diff --git a/src/prompt.ts b/src/prompt.ts index 717991bf..ba9bf231 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -331,8 +331,29 @@ export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDe description: 'Clear, concise description of what this command does in active voice. Never use words like "complex" or "risk" in the description - just describe what it does.', }, + sideEffects: { + description: + 'Permission scopes required by this bash command. Use [] only for commands that do not read, write, delete, or access the network. Use ["unknown"] when the effects cannot be classified safely.', + type: "array", + items: { + type: "string", + enum: [ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "unknown", + ], + }, + uniqueItems: true, + }, }, - required: ["command"], + required: ["command", "sideEffects"], additionalProperties: false, }, }, diff --git a/src/session.ts b/src/session.ts index 3144f883..c5da055c 100644 --- a/src/session.ts +++ b/src/session.ts @@ -22,13 +22,38 @@ import { type CreateOpenAIClient, type ProcessTimeoutControl, type ProcessTimeoutInfo, + type ToolCallExecution, + type ToolExecutionHooks, } from "./tools/executor"; import { McpManager } from "./mcp/mcp-manager"; -import type { McpServerConfig } from "./settings"; +import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; import { logApiError } from "./common/error-logger"; import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; import { killProcessTree } from "./common/process-tree"; import { GitFileHistory } from "./common/file-history"; +import { getSnippet } from "./common/state"; +import { + appendProjectPermissionAllows, + buildPermissionToolExecution, + computeToolCallPermissions, + hasUserPermissionReplies, + normalizeAskPermissions, + parseToolCallForPermissions, + type AskPermissionRequest, + type MessageToolPermission, + type PermissionToolCall, + type UserToolPermission, +} from "./common/permissions"; + +export type { PermissionScope } from "./settings"; +export type { + AskPermissionRequest, + AskPermissionScope, + BashPermissionScope, + MessageToolPermission, + PermissionDecision, + UserToolPermission, +} from "./common/permissions"; const MAX_SESSION_ENTRIES = 50; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; @@ -127,7 +152,14 @@ function getTotalTokens(usage: ModelUsage | null | undefined): number { return typeof totalTokens === "number" ? totalTokens : 0; } -export type SessionStatus = "failed" | "pending" | "processing" | "waiting_for_user" | "completed" | "interrupted"; +export type SessionStatus = + | "failed" + | "pending" + | "processing" + | "waiting_for_user" + | "completed" + | "interrupted" + | "ask_permission"; export type ModelUsage = { prompt_tokens: number; @@ -170,6 +202,7 @@ export type SessionEntry = { createTime: string; updateTime: string; processes: Map | null; // {pid: process info} + askPermissions?: AskPermissionRequest[]; }; export type SessionsIndex = { @@ -188,6 +221,8 @@ export type MessageMeta = { isSummary?: boolean; isModelChange?: boolean; skill?: SkillInfo; + permissions?: MessageToolPermission[]; + userPrompt?: UserPromptContent; }; export type SessionMessage = { @@ -216,6 +251,8 @@ export type UserPromptContent = { text?: string; imageUrls?: string[]; skills?: SkillInfo[]; + permissions?: UserToolPermission[]; + alwaysAllows?: PermissionScope[]; }; export type SkillInfo = { @@ -228,7 +265,12 @@ export type SkillInfo = { type SessionManagerOptions = { projectRoot: string; createOpenAIClient: CreateOpenAIClient; - getResolvedSettings: () => { model: string; webSearchTool?: string; mcpServers?: Record }; + getResolvedSettings: () => { + model: string; + webSearchTool?: string; + mcpServers?: Record; + permissions?: Required; + }; renderMarkdown: (text: string) => string; onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; onSessionEntryUpdated?: (entry: SessionEntry) => void; @@ -253,6 +295,7 @@ export class SessionManager { model: string; webSearchTool?: string; mcpServers?: Record; + permissions?: Required; }; private readonly onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; private readonly onSessionEntryUpdated?: (entry: SessionEntry) => void; @@ -1002,11 +1045,13 @@ ${skillMd} async replySession(sessionId: string, userPrompt: UserPromptContent, controller?: AbortController): Promise { const signal = controller?.signal; this.throwIfAborted(signal); + appendProjectPermissionAllows(this.projectRoot, userPrompt.alwaysAllows); const now = new Date().toISOString(); const updated = this.updateSessionEntry(sessionId, (entry) => ({ ...entry, status: "pending", failReason: null, + askPermissions: undefined, updateTime: now, })); @@ -1015,9 +1060,15 @@ ${skillMd} return; } + if (hasUserPermissionReplies(userPrompt) && this.hasTrailingPendingToolCalls(sessionId)) { + this.activeSessionId = sessionId; + await this.activateSession(sessionId, controller, userPrompt); + return; + } + if (this.isContinuePrompt(userPrompt)) { this.activeSessionId = sessionId; - await this.activateSession(sessionId, controller); + await this.activateSession(sessionId, controller, userPrompt); return; } @@ -1070,7 +1121,11 @@ ${skillMd} ); } - async activateSession(sessionId: string, controller?: AbortController): Promise { + async activateSession( + sessionId: string, + controller?: AbortController, + permissionPrompt?: UserPromptContent + ): Promise { const startedAt = Date.now(); const { client, model, baseURL, thinkingEnabled, reasoningEffort, debugLogEnabled, notify, env } = this.createOpenAIClient(); @@ -1129,16 +1184,20 @@ ${skillMd} return; } - const pendingToolCalls = this.getTrailingPendingToolCalls(this.listSessionMessages(sessionId)); - if (pendingToolCalls.length > 0) { - const toolAppendResult = await this.appendToolMessages(sessionId, pendingToolCalls); + const pendingToolCallMessage = this.getTrailingPendingToolCallMessage(this.listSessionMessages(sessionId)); + if (pendingToolCallMessage.toolCalls.length > 0) { + const toolAppendResult = await this.appendToolMessages(sessionId, pendingToolCallMessage.toolCalls, { + permissionOverrides: permissionPrompt?.permissions, + messagePermissions: pendingToolCallMessage.message?.meta?.permissions, + }); + permissionPrompt = await this.appendDeferredPermissionPrompt(sessionId, permissionPrompt, sessionController); if (this.isInterrupted(sessionId)) { return; } if (toolAppendResult.waitingForUser) { this.updateSessionEntry(sessionId, (entry) => ({ ...entry, - toolCalls: pendingToolCalls, + toolCalls: pendingToolCallMessage.toolCalls, status: "waiting_for_user", updateTime: new Date().toISOString(), })); @@ -1192,12 +1251,47 @@ ${skillMd} return; } const assistantMessage = this.buildAssistantMessage(sessionId, content, toolCalls, thinking); + const permissionPlan = toolCalls + ? computeToolCallPermissions({ + sessionId, + projectRoot: this.projectRoot, + toolCalls, + settings: this.getResolvedSettings().permissions, + resolveSnippetPath: (id, snippetId) => getSnippet(id, snippetId)?.filePath, + }) + : null; + if (permissionPlan) { + assistantMessage.meta = { + ...(assistantMessage.meta ?? {}), + permissions: permissionPlan.permissions, + }; + } this.appendSessionMessage(sessionId, assistantMessage); this.onAssistantMessage(assistantMessage, true); let waitingForUser = false; + const responseUsage = response.usage ?? null; if (toolCalls) { - const toolAppendResult = await this.appendToolMessages(sessionId, toolCalls); + if (permissionPlan?.askPermissions.length) { + this.updateSessionEntry(sessionId, (entry) => ({ + ...entry, + assistantReply: content, + assistantThinking: thinking, + assistantRefusal: refusal, + toolCalls, + usage: accumulateUsage(entry.usage, responseUsage), + usagePerModel: accumulateUsagePerModel(entry.usagePerModel, model, responseUsage), + activeTokens: getTotalTokens(responseUsage), + status: "ask_permission", + failReason: null, + askPermissions: permissionPlan.askPermissions, + updateTime: new Date().toISOString(), + })); + return; + } + const toolAppendResult = await this.appendToolMessages(sessionId, toolCalls, { + messagePermissions: permissionPlan?.permissions, + }); waitingForUser = toolAppendResult.waitingForUser; } @@ -1205,7 +1299,6 @@ ${skillMd} return; } - const responseUsage = response.usage ?? null; this.updateSessionEntry(sessionId, (entry) => ({ ...entry, assistantReply: content, @@ -1217,6 +1310,7 @@ ${skillMd} activeTokens: getTotalTokens(responseUsage), status: refusal ? "failed" : waitingForUser ? "waiting_for_user" : toolCalls ? "processing" : "completed", failReason: refusal ? refusal : entry.failReason, + askPermissions: undefined, updateTime: new Date().toISOString(), })); @@ -1768,6 +1862,7 @@ ${skillMd} visible: true, createTime: now, updateTime: now, + meta: { userPrompt: this.cloneUserPromptForMeta(prompt) }, checkpointHash: this.getCurrentCheckpointHash(sessionId), }; } @@ -1957,8 +2052,15 @@ ${skillMd} }; } - private async appendToolMessages(sessionId: string, toolCalls: unknown[]): Promise<{ waitingForUser: boolean }> { - const toolExecutions = await this.toolExecutor.executeToolCalls(sessionId, toolCalls, { + private async appendToolMessages( + sessionId: string, + toolCalls: unknown[], + options: { + permissionOverrides?: UserToolPermission[]; + messagePermissions?: MessageToolPermission[]; + } = {} + ): Promise<{ waitingForUser: boolean }> { + const hooks: ToolExecutionHooks = { onProcessStart: (pid, command) => this.addSessionProcess(sessionId, pid, command), onProcessExit: (pid) => this.removeSessionProcess(sessionId, pid), onProcessStdout: (pid, chunk) => this.onProcessStdout?.(Number(pid), chunk), @@ -1966,7 +2068,23 @@ ${skillMd} onBeforeFileMutation: (filePath) => this.prepareFileMutationCheckpoint(sessionId, filePath), onAfterFileMutation: (filePath) => this.recordFileMutationCheckpoint(sessionId, filePath), shouldStop: () => this.isInterrupted(sessionId), - }); + }; + const parsedToolCalls = toolCalls + .map((toolCall) => parseToolCallForPermissions(toolCall)) + .filter((toolCall): toolCall is PermissionToolCall => Boolean(toolCall)); + const toolExecutions: ToolCallExecution[] = []; + for (const toolCall of parsedToolCalls) { + if (hooks.shouldStop?.()) { + break; + } + const blockedResult = buildPermissionToolExecution(toolCall, options); + if (blockedResult) { + toolExecutions.push(blockedResult); + continue; + } + const executions = await this.toolExecutor.executeToolCalls(sessionId, [toolCall], hooks); + toolExecutions.push(...executions); + } if (this.isInterrupted(sessionId)) { return { waitingForUser: false }; } @@ -1997,6 +2115,72 @@ ${skillMd} return { waitingForUser }; } + private cloneUserPromptForMeta(prompt: UserPromptContent): UserPromptContent { + return { + text: prompt.text, + imageUrls: prompt.imageUrls ? [...prompt.imageUrls] : undefined, + skills: prompt.skills ? prompt.skills.map((skill) => ({ ...skill })) : undefined, + permissions: prompt.permissions ? prompt.permissions.map((permission) => ({ ...permission })) : undefined, + alwaysAllows: prompt.alwaysAllows ? [...prompt.alwaysAllows] : undefined, + }; + } + + private hasTrailingPendingToolCalls(sessionId: string): boolean { + return this.getTrailingPendingToolCallMessage(this.listSessionMessages(sessionId)).toolCalls.length > 0; + } + + private async appendDeferredPermissionPrompt( + sessionId: string, + userPrompt: UserPromptContent | undefined, + controller: AbortController + ): Promise { + if (!userPrompt || this.isContinuePrompt(userPrompt)) { + return undefined; + } + const text = userPrompt.text ?? ""; + const hasUserContent = + text.trim().length > 0 || + (Array.isArray(userPrompt.imageUrls) && userPrompt.imageUrls.length > 0) || + (Array.isArray(userPrompt.skills) && userPrompt.skills.length > 0); + if (!hasUserContent) { + return undefined; + } + this.reportNewPrompt(); + const signal = controller.signal; + const userMessage = this.buildUserMessage(sessionId, userPrompt); + this.appendSessionMessage(sessionId, userMessage); + if (userPrompt.text) { + const skills = await this.listSkills(sessionId); + const skillNames = await this.identifyMatchingSkillNames(skills, userPrompt.text, { signal, sessionId }); + this.throwIfAborted(signal); + const skillSet = new Set(skillNames); + const matchedSkill = skills.filter((skill) => skillSet.has(skill.name)); + if (Array.isArray(userPrompt.skills)) { + userPrompt.skills.push(...matchedSkill); + } else if (matchedSkill.length > 0) { + userPrompt.skills = matchedSkill; + } + } + userPrompt.skills = await this.normalizeSkills(userPrompt.skills, sessionId); + this.throwIfAborted(signal); + if (userPrompt.skills && userPrompt.skills.length > 0) { + for (const skill of userPrompt.skills) { + if (skill.isLoaded) { + continue; + } + const skillMd = fs.readFileSync(this.resolveSkillPath(skill.path), "utf8"); + const skillPrompt = `Use the skill document below to assist the user:\n +<${skill.name}-skill path="${this.resolveSkillPath(skill.path)}"> +${skillMd} +`; + const skillMessage = this.buildSkillMessage(sessionId, skillPrompt, skill); + this.appendSessionMessage(sessionId, skillMessage); + this.onAssistantMessage(skillMessage, true); + } + } + return undefined; + } + private buildOpenAIMessages( messages: SessionMessage[], thinkingEnabled: boolean, @@ -2125,18 +2309,23 @@ ${skillMd} return pairings; } - private getTrailingPendingToolCalls(messages: SessionMessage[]): unknown[] { + private getTrailingPendingToolCallMessage( + messages: SessionMessage[] + ): { message: SessionMessage; toolCalls: unknown[] } | { message: null; toolCalls: [] } { const activeMessages = messages.filter((message) => !message.compacted); const latestMessage = activeMessages[activeMessages.length - 1]; if (!latestMessage || latestMessage.role !== "assistant") { - return []; + return { message: null, toolCalls: [] }; } const toolCalls = this.getAssistantToolCalls(latestMessage); if (toolCalls.length === 0) { - return []; + return { message: null, toolCalls: [] }; } - return toolCalls.filter((toolCall) => Boolean(this.getToolCallId(toolCall))); + return { + message: latestMessage, + toolCalls: toolCalls.filter((toolCall) => Boolean(this.getToolCallId(toolCall))), + }; } private findPairableToolMessageIndex( @@ -2490,6 +2679,7 @@ ${skillMd} createTime: typeof value.createTime === "string" ? value.createTime : new Date().toISOString(), updateTime: typeof value.updateTime === "string" ? value.updateTime : new Date().toISOString(), processes: this.deserializeProcesses(value.processes), + askPermissions: normalizeAskPermissions(value.askPermissions), }; } @@ -2500,7 +2690,8 @@ ${skillMd} status === "processing" || status === "waiting_for_user" || status === "completed" || - status === "interrupted" + status === "interrupted" || + status === "ask_permission" ) { return status; } diff --git a/src/settings.ts b/src/settings.ts index b5bb869e..e0b17768 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -17,6 +17,27 @@ export type McpServerConfig = { env?: Record; }; +export type PermissionScope = + | "read-in-cwd" + | "read-out-cwd" + | "write-in-cwd" + | "write-out-cwd" + | "delete-in-cwd" + | "delete-out-cwd" + | "query-git-log" + | "mutate-git-log" + | "network" + | "mcp"; + +export type PermissionDefaultMode = "allowAll" | "askAll"; + +export type PermissionSettings = { + allow?: PermissionScope[]; + deny?: PermissionScope[]; + ask?: PermissionScope[]; + defaultMode?: PermissionDefaultMode; +}; + export type DeepcodingSettings = { env?: DeepcodingEnv; model?: string; @@ -26,6 +47,7 @@ export type DeepcodingSettings = { notify?: string; webSearchTool?: string; mcpServers?: Record; + permissions?: PermissionSettings; }; export type ResolvedDeepcodingSettings = { @@ -39,6 +61,7 @@ export type ResolvedDeepcodingSettings = { notify?: string; webSearchTool?: string; mcpServers?: Record; + permissions: Required; }; export type ModelConfigSelection = { @@ -75,6 +98,79 @@ function trimString(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } +const VALID_PERMISSION_SCOPES = new Set([ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "mcp", +]); + +function normalizePermissionList(value: unknown): PermissionScope[] { + if (!Array.isArray(value)) { + return []; + } + const result: PermissionScope[] = []; + for (const item of value) { + if (typeof item !== "string" || !VALID_PERMISSION_SCOPES.has(item as PermissionScope)) { + continue; + } + const scope = item as PermissionScope; + if (!result.includes(scope)) { + result.push(scope); + } + } + return result; +} + +function mergePermissionLists(...lists: Array): PermissionScope[] { + const result: PermissionScope[] = []; + for (const list of lists) { + for (const scope of list ?? []) { + if (!result.includes(scope)) { + result.push(scope); + } + } + } + return result; +} + +function normalizePermissionDefaultMode(value: unknown): PermissionDefaultMode | undefined { + return value === "allowAll" || value === "askAll" ? value : undefined; +} + +function normalizePermissions(settings: PermissionSettings | null | undefined): Required { + return { + allow: normalizePermissionList(settings?.allow), + deny: normalizePermissionList(settings?.deny), + ask: normalizePermissionList(settings?.ask), + defaultMode: normalizePermissionDefaultMode(settings?.defaultMode) ?? "allowAll", + }; +} + +function mergePermissions( + userSettings: DeepcodingSettings | null | undefined, + projectSettings: DeepcodingSettings | null | undefined +): Required { + const userPermissions = normalizePermissions(userSettings?.permissions); + const projectPermissions = normalizePermissions(projectSettings?.permissions); + return { + allow: mergePermissionLists(userPermissions.allow, projectPermissions.allow), + deny: mergePermissionLists(userPermissions.deny, projectPermissions.deny), + ask: mergePermissionLists(userPermissions.ask, projectPermissions.ask), + defaultMode: projectSettings?.permissions + ? projectPermissions.defaultMode + : userSettings?.permissions + ? userPermissions.defaultMode + : "allowAll", + }; +} + function normalizeEnv(env: DeepcodingSettings["env"]): Record { const result: Record = {}; if (!env) { @@ -233,6 +329,7 @@ export function resolveSettingsSources( notify: notify || undefined, webSearchTool: webSearchTool || undefined, mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv), + permissions: mergePermissions(userSettings, projectSettings), }; } diff --git a/src/tests/permissions.test.ts b/src/tests/permissions.test.ts new file mode 100644 index 00000000..adb53885 --- /dev/null +++ b/src/tests/permissions.test.ts @@ -0,0 +1,120 @@ +import { afterEach, test } from "node:test"; +import assert from "node:assert/strict"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { + appendProjectPermissionAllows, + computeToolCallPermissions, + evaluatePermissionScopes, + hasUserPermissionReplies, + parseBashSideEffects, +} from "../common/permissions"; + +const tempDirs: string[] = []; + +afterEach(() => { + while (tempDirs.length > 0) { + const dir = tempDirs.pop(); + if (dir) { + fs.rmSync(dir, { recursive: true, force: true }); + } + } +}); + +test("parseBashSideEffects accepts valid scopes and normalizes unsafe values to unknown", () => { + assert.deepEqual(parseBashSideEffects(["read-in-cwd", "network", "read-in-cwd"]), ["read-in-cwd", "network"]); + assert.deepEqual(parseBashSideEffects(undefined), ["unknown"]); + assert.deepEqual(parseBashSideEffects(["read-in-cwd", "unknown"]), ["unknown"]); + assert.deepEqual(parseBashSideEffects(["mcp"]), ["unknown"]); +}); + +test("evaluatePermissionScopes applies deny, ask, allow, and default mode precedence", () => { + const settings = { + allow: ["read-in-cwd" as const], + deny: ["write-out-cwd" as const], + ask: ["network" as const], + defaultMode: "askAll" as const, + }; + + assert.equal(evaluatePermissionScopes(["write-out-cwd"], settings), "deny"); + assert.equal(evaluatePermissionScopes(["network"], settings), "ask"); + assert.equal(evaluatePermissionScopes(["read-in-cwd"], settings), "allow"); + assert.equal(evaluatePermissionScopes(["write-in-cwd"], settings), "ask"); + assert.equal(evaluatePermissionScopes([], settings), "allow"); + assert.equal(evaluatePermissionScopes(["unknown"], settings), "ask"); +}); + +test("computeToolCallPermissions maps tool calls to permission requests", () => { + const projectRoot = createTempDir("deepcode-permissions-workspace-"); + const plan = computeToolCallPermissions({ + sessionId: "session-1", + projectRoot, + settings: { + allow: [], + deny: [], + ask: ["write-out-cwd", "network"], + defaultMode: "allowAll", + }, + resolveSnippetPath: () => path.join(projectRoot, "src", "file.ts"), + toolCalls: [ + { + id: "call-write", + type: "function", + function: { name: "write", arguments: JSON.stringify({ file_path: "/tmp/out.txt", content: "x" }) }, + }, + { + id: "call-bash", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ command: "curl https://example.com", sideEffects: ["network"] }), + }, + }, + { + id: "call-edit", + type: "function", + function: { name: "edit", arguments: JSON.stringify({ snippet_id: "snippet_1" }) }, + }, + ], + }); + + assert.deepEqual(plan.permissions, [ + { toolCallId: "call-write", permission: "ask" }, + { toolCallId: "call-bash", permission: "ask" }, + { toolCallId: "call-edit", permission: "allow" }, + ]); + assert.deepEqual( + plan.askPermissions.map((item) => ({ id: item.toolCallId, scopes: item.scopes })), + [ + { id: "call-write", scopes: ["write-out-cwd"] }, + { id: "call-bash", scopes: ["network"] }, + ] + ); +}); + +test("appendProjectPermissionAllows writes unique project-level allow scopes", () => { + const projectRoot = createTempDir("deepcode-permission-settings-"); + const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + fs.writeFileSync(settingsPath, JSON.stringify({ permissions: { allow: ["read-in-cwd"] } }), "utf8"); + + appendProjectPermissionAllows(projectRoot, ["read-in-cwd", "write-in-cwd"]); + appendProjectPermissionAllows(projectRoot, ["write-in-cwd"]); + + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + assert.deepEqual(settings.permissions.allow, ["read-in-cwd", "write-in-cwd"]); +}); + +test("hasUserPermissionReplies detects permission reply payloads", () => { + assert.equal(hasUserPermissionReplies({}), false); + assert.equal(hasUserPermissionReplies({ permissions: [] }), false); + assert.equal(hasUserPermissionReplies({ permissions: [{ toolCallId: "call-1", permission: "allow" }] }), true); + assert.equal(hasUserPermissionReplies({ alwaysAllows: ["network"] }), true); +}); + +function createTempDir(prefix: string): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts index cc86712d..953de7ca 100644 --- a/src/tests/prompt.test.ts +++ b/src/tests/prompt.test.ts @@ -19,6 +19,19 @@ test("getTools includes UpdatePlan with string plan schema", () => { assert.equal((tool.function.parameters.properties.plan as { type?: unknown }).type, "string"); }); +test("getTools requires bash sideEffects permission scopes", () => { + const tool = getTools().find((candidate) => candidate.function.name === "bash"); + assert.ok(tool); + assert.deepEqual(tool.function.parameters.required, ["command", "sideEffects"]); + const sideEffects = tool.function.parameters.properties.sideEffects as { + type?: unknown; + items?: { enum?: unknown[] }; + }; + assert.equal(sideEffects.type, "array"); + assert.equal(sideEffects.items?.enum?.includes("write-out-cwd"), true); + assert.equal(sideEffects.items?.enum?.includes("unknown"), true); +}); + test("getSystemPrompt always includes WebSearch docs", () => { const prompt = getSystemPrompt("/tmp/project"); assert.equal(prompt.includes("## WebSearch"), true); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 08d61e96..b3c5de99 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1256,6 +1256,162 @@ test("replySession /continue runs trailing pending tool calls before requesting ); }); +test("activateSession pauses for permission when a tool call requires ask", async () => { + const workspace = createTempDir("deepcode-permission-ask-workspace-"); + const home = createTempDir("deepcode-permission-ask-home-"); + setHomeDir(home); + + const manager = createPermissionSessionManager( + workspace, + [ + { + choices: [ + { + message: { + content: "", + tool_calls: [ + { + id: "call-bash", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ + command: "rg TODO src", + description: "Search TODO markers", + sideEffects: ["read-in-cwd"], + }), + }, + }, + ], + }, + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }, + ], + { + allow: [], + deny: [], + ask: [], + defaultMode: "askAll", + } + ); + + const sessionId = await manager.createSession({ text: "search todos" }); + const session = manager.getSession(sessionId); + const assistant = manager + .listSessionMessages(sessionId) + .find((message) => message.role === "assistant" && (message.messageParams as any)?.tool_calls); + + assert.equal(session?.status, "ask_permission"); + assert.equal(session?.askPermissions?.[0]?.toolCallId, "call-bash"); + assert.deepEqual(session?.askPermissions?.[0]?.scopes, ["read-in-cwd"]); + assert.deepEqual(assistant?.meta?.permissions, [{ toolCallId: "call-bash", permission: "ask" }]); + assert.equal( + manager.listSessionMessages(sessionId).some((message) => message.role === "tool"), + false + ); +}); + +test("replySession applies permission replies, runs pending tools, and stores always allow scopes", async () => { + const workspace = createTempDir("deepcode-permission-allow-workspace-"); + const home = createTempDir("deepcode-permission-allow-home-"); + setHomeDir(home); + fs.writeFileSync(path.join(workspace, "note.txt"), "allowed content\n", "utf8"); + + const manager = createPermissionSessionManager( + workspace, + [createChatResponse("continued", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 })], + { + allow: [], + deny: [], + ask: ["read-in-cwd"], + defaultMode: "allowAll", + } + ); + const originalActivateSession = manager.activateSession.bind(manager); + (manager as any).activateSession = async () => {}; + const sessionId = await manager.createSession({ text: "first prompt" }); + const assistant = (manager as any).buildAssistantMessage( + sessionId, + "Need to read", + [ + { + id: "call-read", + type: "function", + function: { name: "read", arguments: JSON.stringify({ file_path: path.join(workspace, "note.txt") }) }, + }, + ], + null + ) as SessionMessage; + assistant.meta = { ...(assistant.meta ?? {}), permissions: [{ toolCallId: "call-read", permission: "ask" }] }; + (manager as any).appendSessionMessage(sessionId, assistant); + (manager as any).activateSession = originalActivateSession; + + await manager.replySession(sessionId, { + text: "/continue", + permissions: [{ toolCallId: "call-read", permission: "allow" }], + alwaysAllows: ["read-in-cwd"], + }); + + const toolMessage = manager.listSessionMessages(sessionId).find((message) => message.role === "tool"); + const settings = JSON.parse(fs.readFileSync(path.join(workspace, ".deepcode", "settings.json"), "utf8")); + + assert.match(toolMessage?.content ?? "", /allowed content/); + assert.deepEqual(settings.permissions.allow, ["read-in-cwd"]); + assert.equal(manager.getSession(sessionId)?.status, "completed"); +}); + +test("replySession turns denied permission replies into tool errors before appending user text", async () => { + const workspace = createTempDir("deepcode-permission-deny-workspace-"); + const home = createTempDir("deepcode-permission-deny-home-"); + setHomeDir(home); + + const manager = createPermissionSessionManager( + workspace, + [createChatResponse("handled denial", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 })], + { + allow: [], + deny: [], + ask: ["write-out-cwd"], + defaultMode: "allowAll", + } + ); + const originalActivateSession = manager.activateSession.bind(manager); + (manager as any).activateSession = async () => {}; + const sessionId = await manager.createSession({ text: "first prompt" }); + const assistant = (manager as any).buildAssistantMessage( + sessionId, + "Need to write", + [ + { + id: "call-write", + type: "function", + function: { name: "write", arguments: JSON.stringify({ file_path: "/tmp/outside.txt", content: "x" }) }, + }, + ], + null + ) as SessionMessage; + assistant.meta = { ...(assistant.meta ?? {}), permissions: [{ toolCallId: "call-write", permission: "ask" }] }; + (manager as any).appendSessionMessage(sessionId, assistant); + (manager as any).activateSession = originalActivateSession; + + await manager.replySession(sessionId, { + text: "Do not write outside the workspace.", + permissions: [{ toolCallId: "call-write", permission: "deny" }], + }); + + const messages = manager.listSessionMessages(sessionId); + const assistantIndex = messages.findIndex((message) => message.id === assistant.id); + const toolMessage = messages[assistantIndex + 1]; + const userMessage = messages[assistantIndex + 2]; + + assert.equal(toolMessage?.role, "tool"); + assert.match(toolMessage?.content ?? "", /User denied the required permission/); + assert.equal(userMessage?.role, "user"); + assert.equal(userMessage?.content, "Do not write outside the workspace."); +}); + test("replySession preserves raw session messages when a previous tool call is pending", async () => { const workspace = createTempDir("deepcode-pending-tool-workspace-"); const home = createTempDir("deepcode-pending-tool-home-"); @@ -2315,6 +2471,42 @@ function createMockedClientSessionManager(projectRoot: string, responses: unknow }); } +function createPermissionSessionManager( + projectRoot: string, + responses: unknown[], + permissions: { + allow: any[]; + deny: any[]; + ask: any[]; + defaultMode: "allowAll" | "askAll"; + } +): SessionManager { + const client = { + chat: { + completions: { + create: async () => { + const response = responses.shift(); + assert.ok(response, "expected a queued chat response"); + return response; + }, + }, + }, + }; + + return new SessionManager({ + projectRoot, + createOpenAIClient: () => ({ + client: client as any, + model: "test-model", + baseURL: "https://api.deepseek.com", + thinkingEnabled: false, + }), + getResolvedSettings: () => ({ model: "test-model", permissions }), + renderMarkdown: (text) => text, + onAssistantMessage: () => {}, + }); +} + function createMockedClientSessionManagerWithClient(projectRoot: string, client: unknown): SessionManager { return new SessionManager({ projectRoot, diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts index 1707aff8..52f8671d 100644 --- a/src/tests/settings-and-notify.test.ts +++ b/src/tests/settings-and-notify.test.ts @@ -147,6 +147,35 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre assert.equal(resolved.env.WEBHOOK, "system-webhook"); }); +test("resolveSettingsSources merges permission settings", () => { + const resolved = resolveSettingsSources( + { + permissions: { + allow: ["read-in-cwd", "network"], + ask: ["write-out-cwd"], + defaultMode: "askAll", + }, + }, + { + permissions: { + allow: ["write-in-cwd", "read-in-cwd"], + deny: ["delete-out-cwd"], + defaultMode: "allowAll", + }, + }, + { + model: "default-model", + baseURL: "https://default.example.com", + }, + TEST_PROCESS_ENV + ); + + assert.deepEqual(resolved.permissions.allow, ["read-in-cwd", "network", "write-in-cwd"]); + assert.deepEqual(resolved.permissions.ask, ["write-out-cwd"]); + assert.deepEqual(resolved.permissions.deny, ["delete-out-cwd"]); + assert.equal(resolved.permissions.defaultMode, "allowAll"); +}); + test("resolveSettingsSources merges MCP env with documented priority", () => { const resolved = resolveSettingsSources( { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 5419a2ad..c8c24f17 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -8,6 +8,7 @@ import { createOpenAIClient } from "../common/openai-client"; import { type LlmStreamProgress, type MessageMeta, + type PermissionScope, type SessionEntry, SessionManager, type SessionMessage, @@ -38,6 +39,7 @@ import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, } from "./askUserQuestion"; +import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt"; import { buildExitSummaryText } from "./exitSummary"; import { RawMode, useRawModeContext } from "./contexts"; import { renderMessageToStdout } from "./components/MessageView/utils"; @@ -76,6 +78,12 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. const [streamProgress, setStreamProgress] = useState(null); const [runningProcesses, setRunningProcesses] = useState(null); const [activeStatus, setActiveStatus] = useState(null); + const [activeAskPermissions, setActiveAskPermissions] = useState(undefined); + const [pendingPermissionReply, setPendingPermissionReply] = useState<{ + sessionId: string; + permissions: PermissionPromptResult["permissions"]; + alwaysAllows: PermissionScope[]; + } | null>(null); const [dismissedQuestionIds, setDismissedQuestionIds] = useState>(() => new Set()); const [isExiting, setIsExiting] = useState(false); const [showWelcome, setShowWelcome] = useState(true); @@ -105,6 +113,7 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setStatusLine(buildStatusLine(entry)); setRunningProcesses(entry.processes); setActiveStatus(entry.status); + setActiveAskPermissions(entry.askPermissions); }, onLlmStreamProgress: (progress) => { if (progress.phase === "end") { @@ -214,6 +223,8 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setErrorLine(null); setRunningProcesses(null); setActiveStatus(null); + setActiveAskPermissions(undefined); + setPendingPermissionReply(null); setDismissedQuestionIds(new Set()); setShowWelcome(true); setWelcomeNonce((n) => n + 1); @@ -257,7 +268,16 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. imageUrls: submission.imageUrls, skills: submission.selectedSkills && submission.selectedSkills.length > 0 ? submission.selectedSkills : undefined, + permissions: submission.permissions, + alwaysAllows: submission.alwaysAllows, }; + const activeSessionId = sessionManager.getActiveSessionId(); + const permissionReply = + pendingPermissionReply && activeSessionId === pendingPermissionReply.sessionId ? pendingPermissionReply : null; + if (permissionReply) { + prompt.permissions = permissionReply.permissions; + prompt.alwaysAllows = permissionReply.alwaysAllows; + } const trimmedText = (submission.text ?? "").trim(); const selectedSkillNames = submission.selectedSkills?.map((skill) => skill.name).filter(Boolean) ?? []; @@ -277,6 +297,9 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. processStdoutRef.current.clear(); try { await sessionManager.handleUserPrompt(prompt); + if (permissionReply) { + setPendingPermissionReply(null); + } await refreshSkills(); refreshSessionsList(); } catch (error) { @@ -288,7 +311,7 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setRunningProcesses(null); } }, - [exit, onRestart, sessionManager, refreshSkills, refreshSessionsList] + [exit, onRestart, pendingPermissionReply, sessionManager, refreshSkills, refreshSessionsList] ); const handleInterrupt = useCallback(() => { @@ -407,9 +430,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setStatusLine(session ? buildStatusLine(session) : ""); setRunningProcesses(session?.processes ?? null); setActiveStatus(session?.status ?? null); + setActiveAskPermissions(session?.askPermissions); + if (pendingPermissionReply && pendingPermissionReply.sessionId !== sessionId) { + setPendingPermissionReply(null); + } await refreshSkills(sessionId); }, - [sessionManager, refreshSkills] + [pendingPermissionReply, sessionManager, refreshSkills] ); const handleUndoRestore = useCallback( @@ -605,6 +632,39 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setDismissedQuestionIds((prev) => new Set(prev).add(pendingQuestion.messageId)); }, [pendingQuestion]); + const handlePermissionResult = useCallback( + (result: PermissionPromptResult) => { + const sessionId = sessionManager.getActiveSessionId(); + if (!sessionId) { + return; + } + if (result.hasDeny) { + setPendingPermissionReply({ + sessionId, + permissions: result.permissions, + alwaysAllows: result.alwaysAllows, + }); + setStatusLine("Permission denied. Add a reply, then press Enter to continue."); + return; + } + void handlePrompt({ + text: "/continue", + imageUrls: [], + command: "continue", + permissions: result.permissions, + alwaysAllows: result.alwaysAllows, + }); + }, + [handlePrompt, sessionManager] + ); + + const handlePermissionCancel = useCallback(() => { + sessionManager.interruptActiveSession(); + setActiveStatus("interrupted"); + setActiveAskPermissions(undefined); + refreshSessionsList(); + }, [refreshSessionsList, sessionManager]); + if (mode === RawMode.Raw) { return handleRawModeChange(prev)} />; } @@ -683,6 +743,16 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. onSubmit={handleQuestionAnswers} onCancel={handleQuestionCancel} /> + ) : activeStatus === "ask_permission" && + activeAskPermissions && + activeAskPermissions.length > 0 && + !pendingPermissionReply && + !busy ? ( + ) : isExiting ? null : ( void; + onCancel: () => void; +}; + +type ScopePrompt = { + request: AskPermissionRequest; + scope: AskPermissionScope; +}; + +const ALWAYS_ALLOWED_SCOPES = new Set([ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "mcp", +]); + +export function PermissionPrompt({ requests, onSubmit, onCancel }: Props): React.ReactElement | null { + const prompts = useMemo(() => buildScopePrompts(requests), [requests]); + const [index, setIndex] = useState(0); + const [cursor, setCursor] = useState(0); + const [decisions, setDecisions] = useState>({}); + const [alwaysAllows, setAlwaysAllows] = useState([]); + + const effectiveIndex = findNextPromptIndex(prompts, index, alwaysAllows); + const prompt = prompts[effectiveIndex] ?? null; + const options = prompt ? buildOptions(prompt.scope) : []; + + useEffect(() => { + setIndex(0); + setCursor(0); + setDecisions({}); + setAlwaysAllows([]); + }, [requests]); + + useEffect(() => { + if (!prompt) { + onSubmit(buildResult(requests, decisions, alwaysAllows)); + } + }, [alwaysAllows, decisions, onSubmit, prompt, requests]); + + useEffect(() => { + if (cursor >= options.length) { + setCursor(Math.max(0, options.length - 1)); + } + }, [cursor, options.length]); + + useTerminalInput((input, key) => { + if (!prompt) { + return; + } + if (key.escape || (key.ctrl && (input === "c" || input === "C"))) { + onCancel(); + return; + } + if (key.upArrow) { + setCursor((value) => Math.max(0, value - 1)); + return; + } + if (key.downArrow) { + setCursor((value) => Math.min(options.length - 1, value + 1)); + return; + } + if (input && /^[1-3]$/.test(input)) { + const nextCursor = Number(input) - 1; + if (nextCursor >= 0 && nextCursor < options.length) { + commit(options[nextCursor]!.kind); + } + return; + } + if (key.return) { + commit(options[cursor]?.kind ?? "allow"); + } + }); + + if (!prompt) { + return null; + } + + function commit(kind: "allow" | "always" | "deny"): void { + if (!prompt) { + return; + } + if (kind === "always" && isAlwaysAllowedScope(prompt.scope)) { + const scope = prompt.scope; + setAlwaysAllows((prev) => (prev.includes(scope) ? prev : [...prev, scope])); + setDecisions((prev) => ({ + ...prev, + [prompt.request.toolCallId]: prev[prompt.request.toolCallId] === "deny" ? "deny" : "allow", + })); + } else { + setDecisions((prev) => ({ + ...prev, + [prompt.request.toolCallId]: + kind === "deny" ? "deny" : prev[prompt.request.toolCallId] === "deny" ? "deny" : "allow", + })); + } + setIndex(effectiveIndex + 1); + setCursor(0); + } + + return ( + + + + Permission required + + + {" "} + {Math.min(effectiveIndex + 1, prompts.length)}/{prompts.length} + + + {prompt.request.name} + {prompt.request.command} + {prompt.request.description ? {prompt.request.description} : null} + + Do you want to proceed? + + + {options.map((option, optionIndex) => ( + + {optionIndex === cursor ? "> " : " "} + {optionIndex + 1}. {option.label} + + ))} + + + ↑/↓ move · Enter select · Esc interrupt + + + ); +} + +function buildScopePrompts(requests: AskPermissionRequest[]): ScopePrompt[] { + const prompts: ScopePrompt[] = []; + for (const request of requests) { + for (const scope of request.scopes.length > 0 ? request.scopes : ["unknown" as const]) { + prompts.push({ request, scope }); + } + } + return prompts; +} + +function buildOptions(scope: AskPermissionScope): Array<{ kind: "allow" | "always" | "deny"; label: string }> { + const options: Array<{ kind: "allow" | "always" | "deny"; label: string }> = [{ kind: "allow", label: "Yes" }]; + if (isAlwaysAllowedScope(scope)) { + options.push({ kind: "always", label: `Yes, and always allow ${describeScope(scope)}` }); + } + options.push({ kind: "deny", label: "No" }); + return options; +} + +function findNextPromptIndex(prompts: ScopePrompt[], startIndex: number, alwaysAllows: PermissionScope[]): number { + let index = startIndex; + while (index < prompts.length) { + const scope = prompts[index]!.scope; + if (isAlwaysAllowedScope(scope) && alwaysAllows.includes(scope)) { + index += 1; + continue; + } + return index; + } + return prompts.length; +} + +function buildResult( + requests: AskPermissionRequest[], + decisions: Record, + alwaysAllows: PermissionScope[] +): PermissionPromptResult { + const permissions = requests.map((request) => ({ + toolCallId: request.toolCallId, + permission: decisions[request.toolCallId] === "deny" ? ("deny" as const) : ("allow" as const), + })); + return { + permissions, + alwaysAllows, + hasDeny: permissions.some((permission) => permission.permission === "deny"), + }; +} + +function isAlwaysAllowedScope(scope: AskPermissionScope): scope is PermissionScope { + return ALWAYS_ALLOWED_SCOPES.has(scope); +} + +function describeScope(scope: PermissionScope): string { + switch (scope) { + case "read-in-cwd": + return "reads inside this workspace"; + case "read-out-cwd": + return "reads outside this workspace"; + case "write-in-cwd": + return "writes inside this workspace"; + case "write-out-cwd": + return "writes outside this workspace"; + case "delete-in-cwd": + return "deletes inside this workspace"; + case "delete-out-cwd": + return "deletes outside this workspace"; + case "query-git-log": + return "Git history queries"; + case "mutate-git-log": + return "Git history changes"; + case "network": + return "network access"; + case "mcp": + return "MCP tool access"; + default: + return scope; + } +} diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 8897fd36..8c808e9e 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -46,7 +46,7 @@ import { } from "./fileMentions"; import type { FileMentionItem } from "./fileMentions"; import { readClipboardImageAsync } from "./clipboard"; -import type { SessionEntry, SkillInfo } from "../session"; +import type { PermissionScope, SessionEntry, SkillInfo, UserToolPermission } from "../session"; // Re-exported from prompt modules for backward compatibility export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./prompt"; @@ -68,6 +68,8 @@ export type PromptSubmission = { text: string; imageUrls: string[]; selectedSkills?: SkillInfo[]; + permissions?: UserToolPermission[]; + alwaysAllows?: PermissionScope[]; command?: "new" | "resume" | "continue" | "undo" | "mcp" | "exit"; }; diff --git a/templates/tools/bash.md b/templates/tools/bash.md index 07051201..e8597ab9 100644 --- a/templates/tools/bash.md +++ b/templates/tools/bash.md @@ -28,6 +28,11 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. + - The sideEffects argument is required. Declare the minimum permission scopes the command may need. + - Use `sideEffects: []` only for commands that do not read, write, delete, query Git history, mutate Git history, or access the network, such as `date` or `node --version`. + - Use `*-out-cwd` when the command accesses paths outside the current workspace. For example, `cat /etc/hosts` requires `["read-out-cwd"]`. + - Use `query-git-log` for commands such as `git log`, `git show HEAD`, `git blame`, or history diffs. Use `mutate-git-log` for commands such as `git commit`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, or `git tag`. + - Use `["unknown"]` when you cannot classify the command safely. `unknown` must appear alone. - It is very helpful if you write a clear, concise description of what this command does. For simple commands, keep it brief (5-10 words). For complex commands (piped commands, obscure flags, or anything hard to understand at a glance), add enough context to clarify what it does. - If the output exceeds 30000 characters, output will be truncated before being returned to you. - Always prefer using the dedicated tools for these commands: @@ -60,10 +65,31 @@ Usage notes: "description": { "description": "Clear, concise description of what this command does in active voice. Never use words like \"complex\" or \"risk\" in the description - just describe what it does.\n\nFor simple commands (git, npm, standard CLI tools), keep it brief (5-10 words):\n- ls → \"List files in current directory\"\n- git status → \"Show working tree status\"\n- npm install → \"Install package dependencies\"\n\nFor commands that are harder to parse at a glance (piped commands, obscure flags, etc.), add enough context to clarify what it does:\n- find . -name \"*.tmp\" -exec rm {} \\; → \"Find and delete all .tmp files recursively\"\n- git reset --hard origin/main → \"Discard all local changes and match remote main\"\n- curl -s url | jq '.data[]' → \"Fetch JSON from URL and extract data array elements\"", "type": "string" + }, + "sideEffects": { + "description": "Permission scopes required by this bash command. Use [] only for commands that do not read, write, delete, or access the network. Use [\"unknown\"] when the effects cannot be classified safely.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "unknown" + ] + }, + "uniqueItems": true } }, "required": [ - "command" + "command", + "sideEffects" ], "additionalProperties": false } From 90c6b2e7ea6c6c1e343c074c06f00c5c09391d68 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 22 May 2026 21:42:32 +0800 Subject: [PATCH 061/212] chore: update bash.md --- templates/tools/bash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/tools/bash.md b/templates/tools/bash.md index e8597ab9..83027d3f 100644 --- a/templates/tools/bash.md +++ b/templates/tools/bash.md @@ -32,7 +32,7 @@ Usage notes: - Use `sideEffects: []` only for commands that do not read, write, delete, query Git history, mutate Git history, or access the network, such as `date` or `node --version`. - Use `*-out-cwd` when the command accesses paths outside the current workspace. For example, `cat /etc/hosts` requires `["read-out-cwd"]`. - Use `query-git-log` for commands such as `git log`, `git show HEAD`, `git blame`, or history diffs. Use `mutate-git-log` for commands such as `git commit`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, or `git tag`. - - Use `["unknown"]` when you cannot classify the command safely. `unknown` must appear alone. + - Use `["unknown"]` when you cannot classify the command safely. - It is very helpful if you write a clear, concise description of what this command does. For simple commands, keep it brief (5-10 words). For complex commands (piped commands, obscure flags, or anything hard to understand at a glance), add enough context to clarify what it does. - If the output exceeds 30000 characters, output will be truncated before being returned to you. - Always prefer using the dedicated tools for these commands: From 104acff28f6fdc72ee7731813c3aa6069eb73235 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Sat, 23 May 2026 00:02:13 +0800 Subject: [PATCH 062/212] feat: enhance appendProjectPermissionAllows to support inherited permissions --- .gitignore | 1 + src/common/permissions.ts | 35 ++++++++-- src/session.ts | 4 +- src/tests/permissions.test.ts | 120 ++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 11b67ce4..8f054d4b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ .vscode/ *.tgz *.log +.deepcode/settings.json diff --git a/src/common/permissions.ts b/src/common/permissions.ts index e9aae01c..aa87e0d2 100644 --- a/src/common/permissions.ts +++ b/src/common/permissions.ts @@ -360,7 +360,11 @@ export function hasUserPermissionReplies(value: { permissions?: unknown; alwaysA ); } -export function appendProjectPermissionAllows(projectRoot: string, scopes: PermissionScope[] | undefined): void { +export function appendProjectPermissionAllows( + projectRoot: string, + scopes: PermissionScope[] | undefined, + options: { inheritedPermissions?: Required } = {} +): void { if (!Array.isArray(scopes) || scopes.length === 0) { return; } @@ -392,14 +396,35 @@ export function appendProjectPermissionAllows(projectRoot: string, scopes: Permi } catch { settings = {}; } - const currentAllow = Array.isArray(settings.permissions?.allow) ? settings.permissions.allow : []; + + const existingPermissions = settings.permissions; + const permissions: PermissionSettings = existingPermissions + ? { ...existingPermissions } + : options.inheritedPermissions + ? { + allow: [...options.inheritedPermissions.allow], + deny: [...options.inheritedPermissions.deny], + ask: [...options.inheritedPermissions.ask], + defaultMode: options.inheritedPermissions.defaultMode, + } + : {}; + + const currentAllow = Array.isArray(permissions.allow) ? permissions.allow : []; const allow = [...currentAllow]; for (const scope of nextScopes) { if (!allow.includes(scope)) { allow.push(scope); } } - if (allow.length === currentAllow.length) { + const currentDeny = Array.isArray(permissions.deny) ? permissions.deny : undefined; + const currentAsk = Array.isArray(permissions.ask) ? permissions.ask : undefined; + const deny = currentDeny ? currentDeny.filter((scope) => !nextScopes.includes(scope)) : permissions.deny; + const ask = currentAsk ? currentAsk.filter((scope) => !nextScopes.includes(scope)) : permissions.ask; + const changed = + allow.length !== currentAllow.length || + (currentDeny ? (deny as PermissionScope[]).length !== currentDeny.length : false) || + (currentAsk ? (ask as PermissionScope[]).length !== currentAsk.length : false); + if (existingPermissions && !changed) { return; } fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); @@ -409,7 +434,9 @@ export function appendProjectPermissionAllows(projectRoot: string, scopes: Permi { ...settings, permissions: { - ...(settings.permissions ?? {}), + ...permissions, + deny, + ask, allow, }, }, diff --git a/src/session.ts b/src/session.ts index c5da055c..a8a194e6 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1045,7 +1045,9 @@ ${skillMd} async replySession(sessionId: string, userPrompt: UserPromptContent, controller?: AbortController): Promise { const signal = controller?.signal; this.throwIfAborted(signal); - appendProjectPermissionAllows(this.projectRoot, userPrompt.alwaysAllows); + appendProjectPermissionAllows(this.projectRoot, userPrompt.alwaysAllows, { + inheritedPermissions: this.getResolvedSettings().permissions, + }); const now = new Date().toISOString(); const updated = this.updateSessionEntry(sessionId, (entry) => ({ ...entry, diff --git a/src/tests/permissions.test.ts b/src/tests/permissions.test.ts index adb53885..8babf117 100644 --- a/src/tests/permissions.test.ts +++ b/src/tests/permissions.test.ts @@ -106,6 +106,126 @@ test("appendProjectPermissionAllows writes unique project-level allow scopes", ( assert.deepEqual(settings.permissions.allow, ["read-in-cwd", "write-in-cwd"]); }); +test("appendProjectPermissionAllows seeds inherited permissions before adding allow scopes", () => { + const projectRoot = createTempDir("deepcode-permission-settings-default-"); + + appendProjectPermissionAllows(projectRoot, ["query-git-log"], { + inheritedPermissions: { + allow: ["read-in-cwd"], + deny: ["write-out-cwd"], + ask: ["network"], + defaultMode: "askAll", + }, + }); + + const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + assert.deepEqual(settings.permissions, { + allow: ["read-in-cwd", "query-git-log"], + deny: ["write-out-cwd"], + ask: ["network"], + defaultMode: "askAll", + }); +}); + +test("appendProjectPermissionAllows moves inherited ask and deny scopes into allow", () => { + const projectRoot = createTempDir("deepcode-permission-settings-move-inherited-"); + + appendProjectPermissionAllows(projectRoot, ["network", "write-out-cwd"], { + inheritedPermissions: { + allow: ["read-in-cwd"], + deny: ["write-out-cwd"], + ask: ["network", "mcp"], + defaultMode: "askAll", + }, + }); + + const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + assert.deepEqual(settings.permissions, { + allow: ["read-in-cwd", "network", "write-out-cwd"], + deny: [], + ask: ["mcp"], + defaultMode: "askAll", + }); +}); + +test("appendProjectPermissionAllows writes inherited permissions even when scope is already allowed", () => { + const projectRoot = createTempDir("deepcode-permission-settings-inherited-existing-"); + + appendProjectPermissionAllows(projectRoot, ["read-in-cwd"], { + inheritedPermissions: { + allow: ["read-in-cwd"], + deny: [], + ask: ["network"], + defaultMode: "askAll", + }, + }); + + const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + assert.deepEqual(settings.permissions, { + allow: ["read-in-cwd"], + deny: [], + ask: ["network"], + defaultMode: "askAll", + }); +}); + +test("appendProjectPermissionAllows preserves existing project permissions", () => { + const projectRoot = createTempDir("deepcode-permission-settings-explicit-default-"); + const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + fs.writeFileSync( + settingsPath, + JSON.stringify({ permissions: { allow: ["read-in-cwd"], defaultMode: "allowAll" } }), + "utf8" + ); + + appendProjectPermissionAllows(projectRoot, ["query-git-log"], { + inheritedPermissions: { + allow: ["write-in-cwd"], + deny: ["write-out-cwd"], + ask: ["network"], + defaultMode: "askAll", + }, + }); + + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + assert.deepEqual(settings.permissions, { + allow: ["read-in-cwd", "query-git-log"], + defaultMode: "allowAll", + }); +}); + +test("appendProjectPermissionAllows removes existing ask and deny conflicts", () => { + const projectRoot = createTempDir("deepcode-permission-settings-existing-conflict-"); + const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + fs.writeFileSync( + settingsPath, + JSON.stringify({ + permissions: { + allow: ["read-in-cwd"], + deny: ["network", "write-out-cwd"], + ask: ["network", "mcp"], + defaultMode: "askAll", + }, + }), + "utf8" + ); + + appendProjectPermissionAllows(projectRoot, ["network"]); + + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + assert.deepEqual(settings.permissions, { + allow: ["read-in-cwd", "network"], + deny: ["write-out-cwd"], + ask: ["mcp"], + defaultMode: "askAll", + }); +}); + test("hasUserPermissionReplies detects permission reply payloads", () => { assert.equal(hasUserPermissionReplies({}), false); assert.equal(hasUserPermissionReplies({ permissions: [] }), false); From bacb6a4fab37a38be434485eeea194dd36f6b4bc Mon Sep 17 00:00:00 2001 From: xinggitxing Date: Sat, 23 May 2026 18:32:52 +0800 Subject: [PATCH 063/212] feat(ui): add session deletion with Delete key confirmation - Add SessionManager.deleteSession() to remove session index entry and messages file - Add Delete key to trigger session deletion confirmation in SessionList - Two-step confirmation: Enter to confirm, Esc to cancel - Separate backspace (search) and delete (delete trigger) key behavior - Clear active session if deleted session was the active one - Add comprehensive test coverage for deleteSession --- src/session.ts | 22 +++++++ src/tests/session.test.ts | 129 ++++++++++++++++++++++++++++++++++++++ src/ui/App.tsx | 8 +++ src/ui/SessionList.tsx | 71 +++++++++++++++++---- 4 files changed, 218 insertions(+), 12 deletions(-) diff --git a/src/session.ts b/src/session.ts index 54340e72..a3a6dd1d 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1476,6 +1476,28 @@ ${skillMd} return index.entries.find((entry) => entry.id === sessionId) ?? null; } + /** + * Delete a session by its ID. + * Removes the session entry from the index and deletes the associated messages file. + * Returns true if the session was found and deleted, false otherwise. + */ + deleteSession(sessionId: string): boolean { + const index = this.loadSessionsIndex(); + const entryIndex = index.entries.findIndex((entry) => entry.id === sessionId); + if (entryIndex === -1) { + return false; + } + + // Remove from index + index.entries.splice(entryIndex, 1); + this.saveSessionsIndex(index); + + // Remove messages file + this.removeSessionMessages([sessionId]); + + return true; + } + listSessionMessages(sessionId: string): SessionMessage[] { const messagePath = this.getSessionMessagesPath(sessionId); if (!fs.existsSync(messagePath)) { diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index fd831990..a8d943fa 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -2123,6 +2123,135 @@ test("SessionManager adjusts the active Bash timeout control and session metadat assert.equal(processInfo?.deadlineAt, new Date(timeoutInfo.deadlineAtMs).toISOString()); }); +test("SessionManager.deleteSession removes session entry from the index", () => { + const workspace = createTempDir("deepcode-delete-workspace-"); + const home = createTempDir("deepcode-delete-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-delete"); + (manager as any).activateSession = async () => {}; + + // Create two sessions + const session1 = createSessionAndMessages(manager, "session-delete-1", "First session"); + const session2 = createSessionAndMessages(manager, "session-delete-2", "Second session"); + + assert.equal(manager.listSessions().length, 2); + + // Delete the first session + const result = manager.deleteSession(session1); + assert.equal(result, true); + + const remaining = manager.listSessions(); + assert.equal(remaining.length, 1); + assert.equal(remaining[0]?.id, session2); +}); + +test("SessionManager.deleteSession removes the messages file", () => { + const workspace = createTempDir("deepcode-delete-msg-workspace-"); + const home = createTempDir("deepcode-delete-msg-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-delete-msg"); + (manager as any).activateSession = async () => {}; + + const sessionId = createSessionAndMessages(manager, "session-delete-msg", "Test session"); + const messagePath = path.join( + home, + ".deepcode", + "projects", + workspace.replace(/[\\\\/]/g, "-").replace(/:/g, ""), + `${sessionId}.jsonl` + ); + + // Verify messages file exists + assert.ok(fs.existsSync(messagePath)); + + manager.deleteSession(sessionId); + + // Verify messages file is removed + assert.equal(fs.existsSync(messagePath), false); +}); + +test("SessionManager.deleteSession returns false when session does not exist", () => { + const workspace = createTempDir("deepcode-delete-nonexist-workspace-"); + const home = createTempDir("deepcode-delete-nonexist-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-delete-nonexist"); + + const result = manager.deleteSession("nonexistent-session-id"); + assert.equal(result, false); + assert.equal(manager.listSessions().length, 0); +}); + +test("SessionManager.deleteSession does not affect other sessions", () => { + const workspace = createTempDir("deepcode-delete-others-workspace-"); + const home = createTempDir("deepcode-delete-others-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-delete-others"); + (manager as any).activateSession = async () => {}; + + const session1 = createSessionAndMessages(manager, "session-keep-1", "Keep session 1"); + const session2 = createSessionAndMessages(manager, "session-keep-2", "Keep session 2"); + + // Delete non-existent session + const result = manager.deleteSession("non-existent"); + assert.equal(result, false); + assert.equal(manager.listSessions().length, 2); + + // Delete one session + assert.equal(manager.deleteSession(session1), true); + assert.equal(manager.listSessions().length, 1); + assert.equal(manager.listSessions()[0]?.id, session2); + + // The remaining session should still have its messages accessible + const messages = manager.listSessionMessages(session2); + assert.ok(messages.length > 0); +}); + +/** + * Helper: creates a session and writes a few messages to it so we can test + * that deleteSession removes both the index entry and the messages file. + */ +function createSessionAndMessages(manager: SessionManager, sessionId: string, summary: string): string { + const now = new Date().toISOString(); + const index = (manager as any).loadSessionsIndex(); + index.entries.push({ + id: sessionId, + summary, + assistantReply: null, + assistantThinking: null, + assistantRefusal: null, + toolCalls: null, + status: "completed", + failReason: null, + usage: null, + usagePerModel: null, + activeTokens: 0, + createTime: now, + updateTime: now, + processes: null, + }); + (manager as any).saveSessionsIndex(index); + + // Write a couple of message lines to the messages file + const projectDir = (manager as any).getProjectStorage().projectDir; + const messagePath = path.join(projectDir, `${sessionId}.jsonl`); + const msg = JSON.stringify({ + id: "msg-1", + sessionId, + role: "user", + content: summary, + visible: true, + createTime: now, + updateTime: now, + }); + fs.writeFileSync(messagePath, `${msg}\n`, "utf8"); + + return sessionId; +} + function hasGit(): boolean { try { execFileSync("git", ["--version"], { stdio: "ignore" }); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 5419a2ad..942bbf8b 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -658,6 +658,14 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. sessions={sessions} onSelect={(id) => void handleSelectSession(id)} onCancel={() => setView("chat")} + onDelete={(id) => { + // If the deleted session is the active one, clear it + if (sessionManager.getActiveSessionId() === id) { + sessionManager.setActiveSessionId(null); + } + sessionManager.deleteSession(id); + refreshSessionsList(); + }} /> ) : view === "undo" ? ( void; onCancel: () => void; + onDelete?: (sessionId: string) => void; }; /** @@ -36,9 +37,10 @@ export function filterSessions(sessions: SessionEntry[], query: string): Session }); } -export function SessionList({ sessions, onSelect, onCancel }: Props): React.ReactElement { +export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): React.ReactElement { const [index, setIndex] = useState(0); const [searchQuery, setSearchQuery] = useState(""); + const [confirmDeleteSessionId, setConfirmDeleteSessionId] = useState(null); const { columns, rows } = useWindowSize(); // Filter sessions by search query @@ -77,7 +79,23 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac setIndex(0); }, []); + const selectedSession = filteredSessions[safeIndex]; + useInput((input, key) => { + // If in delete confirmation mode, handle confirm/cancel + if (confirmDeleteSessionId) { + if (key.return) { + onDelete?.(confirmDeleteSessionId); + setConfirmDeleteSessionId(null); + return; + } + if (key.escape) { + setConfirmDeleteSessionId(null); + return; + } + return; + } + // ESC: clear search first, then cancel if (key.escape) { if (searchQuery) { @@ -95,13 +113,25 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac return; } - // Backspace / Delete: remove last search character - if (key.backspace || key.delete) { + // Backspace: remove last search character + if (key.backspace) { + if (searchQuery) { + handleBackspace(); + return; + } + } + + // Delete key: remove search character, or start delete confirmation + if (key.delete) { if (searchQuery) { handleBackspace(); return; } - // If no search query, navigation keys below handle the rest + // No search query: start delete confirmation if session is selected + if (selectedSession && onDelete) { + setConfirmDeleteSessionId(selectedSession.id); + return; + } } // Printable character: append to search query @@ -211,20 +241,23 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac ) : ( visibleSessions.map((session, i) => { const actualIndex = scrollOffset + i; + const isSelected = actualIndex === safeIndex; + const isConfirming = confirmDeleteSessionId === session.id; return ( - {actualIndex === safeIndex ? "> " : " "} + {isSelected ? "> " : " "} - + {formatSessionTitle(session.summary || "Untitled")} - ({formatSessionStatus(session.status)}) + {isConfirming ? ( + [Delete? Enter=yes, Esc=no] + ) : ( + ({formatSessionStatus(session.status)}) + )} {formatTimestamp(session.updateTime)} @@ -245,14 +278,28 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac {/* Footer */} - {hasActiveSearch ? ( + {confirmDeleteSessionId ? ( + + Delete this session? + + Enter + + to confirm · + + Esc + + to cancel + + ) : hasActiveSearch ? ( Esc clear search · ↑/↓ navigate · Enter select · Esc again to cancel ) : ( - Type to search · ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel + + Type to search · ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel · Del delete + )} From 928551e127b0df5f77b44aaee030863f838406e0 Mon Sep 17 00:00:00 2001 From: dengm Date: Sat, 23 May 2026 19:58:33 +0800 Subject: [PATCH 064/212] feat: add closed-border markdown table rendering with CJK/emoji support - Detect markdown tables and render with Unicode box-drawing characters - Calculate visual terminal width for CJK/emoji (2 cols) vs ASCII (1 col) - Wrap long cells across multiple lines, prefer word-boundary breaks - Allocate column widths: narrow columns (#, status, count, date) minimal, content columns kept >= 12 chars - Render tables with to prevent Ink from breaking box-drawing lines at cell boundary spaces - Expose renderMarkdownSegments() for per-segment wrapping control --- Screenshot_2026-05-23_195028.png | Bin 0 -> 105561 bytes src/ui/components/MessageView/index.tsx | 17 +- src/ui/components/MessageView/markdown.ts | 324 +++++++++++++++++++--- src/ui/index.ts | 2 +- 4 files changed, 304 insertions(+), 39 deletions(-) create mode 100644 Screenshot_2026-05-23_195028.png diff --git a/Screenshot_2026-05-23_195028.png b/Screenshot_2026-05-23_195028.png new file mode 100644 index 0000000000000000000000000000000000000000..870fbaae9e6cb8f3940673faac16d3811fea2f41 GIT binary patch literal 105561 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 - - {content ? {renderMarkdown(content)} : null} + + {content + ? renderMarkdownSegments(content, Math.max(20, contentWidth - 4)).map((seg, i) => { + if (seg.kind === "table") { + return ( + + {seg.body} + + ); + } + return {seg.body}; + }) + : null} ); diff --git a/src/ui/components/MessageView/markdown.ts b/src/ui/components/MessageView/markdown.ts index 11fb0eaa..8c865343 100644 --- a/src/ui/components/MessageView/markdown.ts +++ b/src/ui/components/MessageView/markdown.ts @@ -1,22 +1,61 @@ import chalk from "chalk"; -export function renderMarkdown(text: string): string { - if (!text) { - return ""; - } +/** + * A rendered piece of markdown. Consumers should use `wrap="truncate-end"` for + * `table` segments and the default wrap mode for `text` segments so that Ink + * never breaks box-drawing lines at cell boundary spaces. + */ +export type MarkdownSegment = + | { kind: "text"; body: string } + | { kind: "table"; body: string } + | { kind: "code"; body: string; lang: string }; + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** Render markdown to a single string (backward-compatible). */ +export function renderMarkdown(text: string, maxWidth?: number): string { + return renderMarkdownSegments(text, maxWidth) + .map((s) => s.body) + .join(""); +} + +/** Render markdown, returning typed segments so the caller can choose the + right `` per segment. */ +export function renderMarkdownSegments(text: string, maxWidth?: number): MarkdownSegment[] { + if (!text) return []; + const segments: MarkdownSegment[] = []; const fenceSegments = splitByFences(text); - return fenceSegments - .map((segment) => { - if (segment.kind === "code") { - const langTag = segment.lang ? chalk.dim(`[${segment.lang}]`) + "\n" : ""; - return langTag + chalk.cyan(segment.body); + + for (const seg of fenceSegments) { + if (seg.kind === "code") { + const langTag = seg.lang ? chalk.dim(`[${seg.lang}]`) + "\n" : ""; + segments.push({ kind: "code", body: langTag + chalk.cyan(seg.body), lang: seg.lang }); + continue; + } + const blocks = splitTableBlocks(seg.body); + for (const b of blocks) { + if (b.kind === "table") { + segments.push({ kind: "table", body: renderTableBorder(b.rows, maxWidth) }); + } else { + const body = b.body + .split("\n") + .map((line) => renderInlineLine(line)) + .join("\n"); + if (body) segments.push({ kind: "text", body }); } - return renderInlineBlock(segment.body); - }) - .join(""); + } + } + + return segments; } +// --------------------------------------------------------------------------- +// Code fences +// --------------------------------------------------------------------------- + type FenceSegment = { kind: "text"; body: string } | { kind: "code"; lang: string; body: string }; function splitByFences(text: string): FenceSegment[] { @@ -28,35 +67,27 @@ function splitByFences(text: string): FenceSegment[] { let fenceBody: string[] = []; const flushText = () => { - if (buffer.length === 0) { - return; + if (buffer.length > 0) { + segments.push({ kind: "text", body: buffer.join("\n") }); + buffer = []; } - segments.push({ kind: "text", body: buffer.join("\n") }); - buffer = []; }; for (const line of lines) { - const fenceMatch = /^\s*```(\w*)\s*$/.exec(line); - if (fenceMatch) { + const m = /^\s*```(\w*)\s*$/.exec(line); + if (m) { if (!inFence) { flushText(); inFence = true; - fenceLang = fenceMatch[1] ?? ""; + fenceLang = m[1] ?? ""; fenceBody = []; } else { segments.push({ kind: "code", lang: fenceLang, body: fenceBody.join("\n") }); inFence = false; - fenceLang = ""; - fenceBody = []; } continue; } - - if (inFence) { - fenceBody.push(line); - } else { - buffer.push(line); - } + (inFence ? fenceBody : buffer).push(line); } if (inFence) { @@ -68,13 +99,238 @@ function splitByFences(text: string): FenceSegment[] { return segments; } -function renderInlineBlock(text: string): string { - return text - .split("\n") - .map((line) => renderInlineLine(line)) - .join("\n"); +// --------------------------------------------------------------------------- +// Table parsing +// --------------------------------------------------------------------------- + +type TableBlock = { kind: "text"; body: string } | { kind: "table"; rows: string[][] }; + +function splitTableBlocks(text: string): TableBlock[] { + const lines = text.split(/\r?\n/); + const blocks: TableBlock[] = []; + let buffer: string[] = []; + let tableRows: string[][] = []; + let inTable = false; + + const flushText = () => { + if (buffer.length > 0) { + blocks.push({ kind: "text", body: buffer.join("\n") }); + buffer = []; + } + }; + const flushTable = () => { + if (tableRows.length >= 2) { + blocks.push({ kind: "table", rows: tableRows }); + } else if (tableRows.length > 0) { + buffer.push(...tableRows.map((r) => r.join(" | "))); + } + tableRows = []; + }; + + const sepRe = /^\|?\s*:?[-]{3,}:?\s*(\|\s*:?[-]{3,}:?\s*)*\|?\s*$/; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + const nextTrimmed = (lines[i + 1] ?? "").trim(); + + // skip separator line + if (inTable && sepRe.test(trimmed) && tableRows.length === 1) continue; + + const isRow = /^\|.+\|$/.test(trimmed); + const isHeader = isRow && i + 1 < lines.length && sepRe.test(nextTrimmed); + + if (isHeader && !inTable) { + flushText(); + inTable = true; + tableRows = [ + trimmed + .split("|") + .filter(Boolean) + .map((s) => s.trim()), + ]; + continue; + } + + if (isRow && inTable) { + tableRows.push( + trimmed + .split("|") + .filter(Boolean) + .map((s) => s.trim()) + ); + continue; + } + + if (inTable && !isRow) { + flushTable(); + inTable = false; + } + buffer.push(line); + } + + return inTable ? [...blocks, ...flushTableResult(tableRows)] : [...blocks, ...flushTextOnly(buffer, tableRows)]; +} + +function flushTableResult(rows: string[][]): TableBlock[] { + if (rows.length >= 2) return [{ kind: "table", rows }]; + if (rows.length > 0) return [{ kind: "text", body: rows.map((r) => r.join(" | ")).join("\n") }]; + return []; +} + +function flushTextOnly(buffer: string[], tableRows: string[][]): TableBlock[] { + const result: TableBlock[] = []; + if (buffer.length > 0) result.push({ kind: "text", body: buffer.join("\n") }); + if (tableRows.length >= 2) result.push({ kind: "table", rows: tableRows }); + else if (tableRows.length > 0) result.push({ kind: "text", body: tableRows.map((r) => r.join(" | ")).join("\n") }); + return result; +} + +// --------------------------------------------------------------------------- +// Terminal visual width (CJK / emoji = 2 cols, ASCII = 1) +// --------------------------------------------------------------------------- + +function visualWidth(text: string): number { + let w = 0; + for (const ch of text) { + if (ch.length >= 2) { + w += 2; + continue; + } + const code = ch.codePointAt(0) ?? ch.charCodeAt(0); + w += isWideChar(code) ? 2 : 1; + } + return w; +} + +function isWideChar(code: number): boolean { + return ( + (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo + (code >= 0x2329 && code <= 0x232a) || // Misc technical + (code >= 0x2e80 && code <= 0xa4cf) || // CJK Radicals, Kangxi, CJK all + (code >= 0xac00 && code <= 0xd7af) || // Hangul Syllables + (code >= 0xf900 && code <= 0xfaff) || // CJK Compat + (code >= 0xfe10 && code <= 0xfe6f) || // CJK Compat Forms + (code >= 0xff00 && code <= 0xffe6) || // Fullwidth + (code >= 0x20000 && code <= 0x3fffd) || // CJK Ext B+ + (code >= 0x1f300 && code <= 0x1faff) || // Emoji & pictographs + (code >= 0x2600 && code <= 0x27bf) || // Misc Symbols + (code >= 0x2300 && code <= 0x23ff) || // Misc Technical + (code >= 0x2b00 && code <= 0x2bff) || // Misc Symbols & Arrows + (code >= 0x1f000 && code <= 0x1f02f) // Mahjong & Domino + ); +} + +// --------------------------------------------------------------------------- +// Table rendering +// --------------------------------------------------------------------------- + +function renderTableBorder(rows: string[][], maxWidth?: number): string { + if (rows.length === 0) return ""; + + const colCount = rows[0].length; + const calcW = (cs: number[]) => cs.reduce((a, b) => a + b + 2, 0) + cs.length + 1; + + // Ideal widths — longest word / 1.5 so cells can wrap in 2-3 lines + const ideal: number[] = Array.from({ length: colCount }, (_, i) => { + const texts = rows.map((r) => r[i] ?? ""); + const maxLine = Math.max(...texts.map((t) => visualWidth(t))); + const words = texts.flatMap((t) => t.split(/\s+/)); + const maxWord = Math.max(4, ...words.map((w) => visualWidth(w))); + return Math.max(maxWord + 2, Math.ceil(maxLine / 1.5)); + }); + + const colWidths = [...ideal]; + + // Shrink to fit terminal width + if (maxWidth != null && calcW(colWidths) > maxWidth) { + const narrow = new Set([0, 1, colCount - 2, colCount - 1]); // #, status, count, date + const MIN_NARROW = 6; + const MIN_CONTENT = 12; + const contentCols = Array.from({ length: colCount }, (_, i) => i).filter((i) => !narrow.has(i)); + + // Cap narrow columns first + for (const ci of narrow) colWidths[ci] = Math.min(colWidths[ci], MIN_NARROW); + + // Shrink until we fit + while (calcW(colWidths) > maxWidth) { + // Try narrow columns first + let shrunk = false; + for (const ci of narrow) { + if (colWidths[ci] > 4 && calcW(colWidths) > maxWidth) { + colWidths[ci]--; + shrunk = true; + } + } + if (shrunk) continue; + // Then content columns + const widest = contentCols.reduce((a, b) => (colWidths[a] > colWidths[b] ? a : b), contentCols[0]); + if (colWidths[widest] > MIN_CONTENT) colWidths[widest]--; + else break; + } + } + + // Word-wrap a single cell + const wrapCell = (text: string, width: number): string[] => { + if (!text) return [""]; + const lines: string[] = []; + let cur = ""; + const flush = () => { + if (cur.trim()) lines.push(cur.replace(/\s+$/, "")); + cur = ""; + }; + + for (const ch of text) { + const cw = visualWidth(ch); + if (visualWidth(cur) + cw > width) { + const lastSpace = cur.lastIndexOf(" "); + if (lastSpace > width / 3) { + const carry = cur.slice(lastSpace + 1); + cur = cur.slice(0, lastSpace); + flush(); + cur = carry + ch; + } else { + flush(); + cur = ch; + } + } else { + cur += ch; + } + } + if (cur.trim()) lines.push(cur.replace(/\s+$/, "")); + return lines.length > 0 ? lines : [""]; + }; + + const wrapped = rows.map((r) => r.map((c, ci) => wrapCell(c, colWidths[ci]))); + const heights = wrapped.map((wr) => Math.max(1, ...wr.map((lines) => lines.length))); + + const pad = (s: string, w: number) => s + " ".repeat(Math.max(0, w - visualWidth(s))); + + const top = "┌" + colWidths.map((w) => "─".repeat(w + 2)).join("┬") + "┐"; + const hdr = "├" + colWidths.map((w) => "─".repeat(w + 2)).join("┼") + "┤"; + const sep = "├" + colWidths.map((w) => "─".repeat(w + 2)).join("┼") + "┤"; + const bot = "└" + colWidths.map((w) => "─".repeat(w + 2)).join("┴") + "┘"; + + const out: string[] = [top]; + + for (let ri = 0; ri < wrapped.length; ri++) { + const h = heights[ri]; + for (let li = 0; li < h; li++) { + const line = wrapped[ri].map((cellLines, ci) => " " + pad(cellLines[li] ?? "", colWidths[ci]) + " "); + out.push("│" + line.join("│") + "│"); + } + if (ri === 0 && rows.length > 1) out.push(hdr); + else if (ri < rows.length - 1) out.push(sep); + } + + out.push(bot); + return out.join("\n"); } +// --------------------------------------------------------------------------- +// Inline formatting (headings, lists, quotes, bold/italic/code) +// --------------------------------------------------------------------------- + function renderInlineLine(line: string): string { const headingMatch = /^(\s*)(#{1,6})\s+(.*)$/.exec(line); if (headingMatch) { @@ -105,9 +361,7 @@ function renderInlineLine(line: string): string { } function renderInlineSpans(text: string): string { - if (!text) { - return text; - } + if (!text) return text; let result = text; result = result.replace(/`([^`]+)`/g, (_, inner) => chalk.cyan(inner)); result = result.replace(/\*\*([^*]+)\*\*/g, (_, inner) => chalk.bold(inner)); diff --git a/src/ui/index.ts b/src/ui/index.ts index d899d4b4..13489037 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -54,7 +54,7 @@ export { } from "./askUserQuestion"; export { readClipboardImage, type ClipboardImage } from "./clipboard"; export { buildLoadingText, type LoadingTextInput } from "./loadingText"; -export { renderMarkdown } from "./components/MessageView/markdown"; +export { renderMarkdown, renderMarkdownSegments, type MarkdownSegment } from "./components/MessageView/markdown"; export { EMPTY_BUFFER, insertText, From 809670952601f7dedf738008190b9d8d84054491 Mon Sep 17 00:00:00 2001 From: dengmik-commits Date: Sat, 23 May 2026 20:05:13 +0800 Subject: [PATCH 065/212] add screenshot --- Screenshot_2026-05-23_195028.png | Bin 105561 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Screenshot_2026-05-23_195028.png b/Screenshot_2026-05-23_195028.png index 870fbaae9e6cb8f3940673faac16d3811fea2f41..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 105561 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 Date: Mon, 25 May 2026 08:46:26 +0800 Subject: [PATCH 066/212] feat: add getScopeRiskColor function for permission scope risk mapping --- src/common/permissions.ts | 35 ++++++++++++++++++- src/tests/permission-prompt.test.ts | 19 +++++++++++ src/tests/permissions.test.ts | 33 ++++++++++++++++++ src/ui/PermissionPrompt.tsx | 53 ++++++++++++++++++++++++++--- 4 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 src/tests/permission-prompt.test.ts diff --git a/src/common/permissions.ts b/src/common/permissions.ts index aa87e0d2..564bfeb8 100644 --- a/src/common/permissions.ts +++ b/src/common/permissions.ts @@ -164,9 +164,10 @@ export function computeToolCallPermissions(options: ComputeToolCallPermissionsOp const permission = evaluatePermissionScopes(request.scopes, options.settings); permissions.push({ toolCallId: toolCall.id, permission }); if (permission === "ask") { + const askScopes = getPermissionScopesRequiringAsk(request.scopes, options.settings); askPermissions.push({ toolCallId: toolCall.id, - scopes: request.scopes, + scopes: askScopes.length > 0 ? askScopes : request.scopes, name: request.name, command: request.command, description: request.description, @@ -285,6 +286,38 @@ export function evaluatePermissionScopes( return settings.defaultMode === "askAll" ? "ask" : "allow"; } +export function getPermissionScopesRequiringAsk( + scopes: AskPermissionScope[], + settings: Required = { + allow: [], + deny: [], + ask: [], + defaultMode: "allowAll", + } +): AskPermissionScope[] { + const result: AskPermissionScope[] = []; + for (const scope of scopes) { + if (scope === "unknown") { + result.push(scope); + continue; + } + if (settings.deny.includes(scope)) { + continue; + } + if (settings.ask.includes(scope)) { + result.push(scope); + continue; + } + if (settings.allow.includes(scope)) { + continue; + } + if (settings.defaultMode === "askAll") { + result.push(scope); + } + } + return result; +} + export function parseBashSideEffects(value: unknown): AskPermissionScope[] { const validScopes = new Set([ "read-in-cwd", diff --git a/src/tests/permission-prompt.test.ts b/src/tests/permission-prompt.test.ts new file mode 100644 index 00000000..aa4f372d --- /dev/null +++ b/src/tests/permission-prompt.test.ts @@ -0,0 +1,19 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { getScopeRiskColor } from "../ui/PermissionPrompt"; + +test("getScopeRiskColor maps permission scopes by risk", () => { + assert.equal(getScopeRiskColor("read-in-cwd"), "#22c55e"); + assert.equal(getScopeRiskColor("query-git-log"), "#22c55e"); + + assert.equal(getScopeRiskColor("read-out-cwd"), "#f59e0b"); + assert.equal(getScopeRiskColor("write-in-cwd"), "#f59e0b"); + assert.equal(getScopeRiskColor("network"), "#f59e0b"); + assert.equal(getScopeRiskColor("mcp"), "#f59e0b"); + + assert.equal(getScopeRiskColor("write-out-cwd"), "#ef4444"); + assert.equal(getScopeRiskColor("delete-in-cwd"), "#ef4444"); + assert.equal(getScopeRiskColor("delete-out-cwd"), "#ef4444"); + assert.equal(getScopeRiskColor("mutate-git-log"), "#ef4444"); + assert.equal(getScopeRiskColor("unknown"), "#ef4444"); +}); diff --git a/src/tests/permissions.test.ts b/src/tests/permissions.test.ts index 8babf117..3a286167 100644 --- a/src/tests/permissions.test.ts +++ b/src/tests/permissions.test.ts @@ -93,6 +93,39 @@ test("computeToolCallPermissions maps tool calls to permission requests", () => ); }); +test("computeToolCallPermissions only asks for scopes not already allowed", () => { + const projectRoot = createTempDir("deepcode-permissions-filter-workspace-"); + const plan = computeToolCallPermissions({ + sessionId: "session-1", + projectRoot, + settings: { + allow: ["read-in-cwd"], + deny: [], + ask: [], + defaultMode: "askAll", + }, + toolCalls: [ + { + id: "call-bash", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ + command: "curl -s http://localhost:8899/ && ls index.html", + sideEffects: ["network", "read-in-cwd"], + }), + }, + }, + ], + }); + + assert.deepEqual(plan.permissions, [{ toolCallId: "call-bash", permission: "ask" }]); + assert.deepEqual( + plan.askPermissions.map((item) => ({ id: item.toolCallId, scopes: item.scopes })), + [{ id: "call-bash", scopes: ["network"] }] + ); +}); + test("appendProjectPermissionAllows writes unique project-level allow scopes", () => { const projectRoot = createTempDir("deepcode-permission-settings-"); const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); diff --git a/src/ui/PermissionPrompt.tsx b/src/ui/PermissionPrompt.tsx index 4613639f..03881a58 100644 --- a/src/ui/PermissionPrompt.tsx +++ b/src/ui/PermissionPrompt.tsx @@ -20,6 +20,13 @@ type ScopePrompt = { scope: AskPermissionScope; }; +type PromptOption = { + kind: "allow" | "always" | "deny"; + label: string; + scopeDescription?: string; + scopeColor?: string; +}; + const ALWAYS_ALLOWED_SCOPES = new Set([ "read-in-cwd", "read-out-cwd", @@ -138,7 +145,7 @@ export function PermissionPrompt({ requests, onSubmit, onCancel }: Props): React {options.map((option, optionIndex) => ( {optionIndex === cursor ? "> " : " "} - {optionIndex + 1}. {option.label} + {optionIndex + 1}. {renderOptionLabel(option)} ))} @@ -149,6 +156,18 @@ export function PermissionPrompt({ requests, onSubmit, onCancel }: Props): React ); } +function renderOptionLabel(option: PromptOption): React.ReactNode { + if (option.scopeDescription && option.scopeColor) { + return ( + <> + {option.label} + {option.scopeDescription} + + ); + } + return option.label; +} + function buildScopePrompts(requests: AskPermissionRequest[]): ScopePrompt[] { const prompts: ScopePrompt[] = []; for (const request of requests) { @@ -159,10 +178,15 @@ function buildScopePrompts(requests: AskPermissionRequest[]): ScopePrompt[] { return prompts; } -function buildOptions(scope: AskPermissionScope): Array<{ kind: "allow" | "always" | "deny"; label: string }> { - const options: Array<{ kind: "allow" | "always" | "deny"; label: string }> = [{ kind: "allow", label: "Yes" }]; +function buildOptions(scope: AskPermissionScope): PromptOption[] { + const options: PromptOption[] = [{ kind: "allow", label: "Yes" }]; if (isAlwaysAllowedScope(scope)) { - options.push({ kind: "always", label: `Yes, and always allow ${describeScope(scope)}` }); + options.push({ + kind: "always", + label: "Yes, and always allow ", + scopeDescription: describeScope(scope), + scopeColor: getScopeRiskColor(scope), + }); } options.push({ kind: "deny", label: "No" }); return options; @@ -201,6 +225,27 @@ function isAlwaysAllowedScope(scope: AskPermissionScope): scope is PermissionSco return ALWAYS_ALLOWED_SCOPES.has(scope); } +export function getScopeRiskColor(scope: AskPermissionScope): string { + switch (scope) { + case "read-in-cwd": + case "query-git-log": + return "#22c55e"; + case "read-out-cwd": + case "write-in-cwd": + case "network": + case "mcp": + return "#f59e0b"; + case "write-out-cwd": + case "delete-in-cwd": + case "delete-out-cwd": + case "mutate-git-log": + case "unknown": + return "#ef4444"; + default: + return "#ef4444"; + } +} + function describeScope(scope: PermissionScope): string { switch (scope) { case "read-in-cwd": From 5d6f727eb686ebdf662e715fe8513c5df05adfbb Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 25 May 2026 11:15:09 +0800 Subject: [PATCH 067/212] feat: add permission.md and update README.md --- README-en.md | 4 ++ README-zh_CN.md | 5 +++ README.md | 5 +++ docs/permission.md | 101 ++++++++++++++++++++++++++++++++++++++++++ docs/permission_en.md | 100 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 docs/permission.md create mode 100644 docs/permission_en.md diff --git a/README-en.md b/README-en.md index 4bff6afc..c1d4acb9 100644 --- a/README-en.md +++ b/README-en.md @@ -137,6 +137,10 @@ When the AI assistant completes a task, Deep Code can automatically execute a no For detailed configuration instructions, see: [docs/notify_en.md](docs/notify_en.md) +### Does Deep Code only support YOLO mode? + +No. Deep Code has a built-in fine-grained permission control mechanism that lets you confirm operations before the AI assistant executes shell commands, reads/writes files, accesses the network, and more. You can configure each permission scope's policy — always allow, always ask, or deny — via the `permissions` field in `settings.json`. See [docs/permission.md](docs/permission.md) for details. + ## Contributing Contributions are welcome! Here's how to get started: diff --git a/README-zh_CN.md b/README-zh_CN.md index 77db4971..26437563 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -122,6 +122,10 @@ Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览 详细配置指南:[docs/notify.md](docs/notify.md) +### Deep Code 只支持 YOLO 模式吗? + +不是。Deep Code 内置了细粒度的权限控制机制,支持在 AI 助手执行 Shell 命令、读写文件、访问网络等操作前进行确认。你可以通过 `settings.json` 中的 `permissions` 字段按需配置每种权限范围的策略:始终允许、始终询问、或直接拒绝。详见 [docs/permission.md](docs/permission.md)。 + ### 是否支持 Coding Plan? 支持。只要把 `~/.deepcode/settings.json` 的 `env.BASE_URL` 配置为 OpenAI 兼容的接口地址就行。以火山方舟的 Coding Plan 为例: @@ -136,6 +140,7 @@ Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览 "thinkingEnabled": true } ``` + ## 贡献 欢迎贡献代码!以下是参与方式: diff --git a/README.md b/README.md index 77db4971..26437563 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,10 @@ Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览 详细配置指南:[docs/notify.md](docs/notify.md) +### Deep Code 只支持 YOLO 模式吗? + +不是。Deep Code 内置了细粒度的权限控制机制,支持在 AI 助手执行 Shell 命令、读写文件、访问网络等操作前进行确认。你可以通过 `settings.json` 中的 `permissions` 字段按需配置每种权限范围的策略:始终允许、始终询问、或直接拒绝。详见 [docs/permission.md](docs/permission.md)。 + ### 是否支持 Coding Plan? 支持。只要把 `~/.deepcode/settings.json` 的 `env.BASE_URL` 配置为 OpenAI 兼容的接口地址就行。以火山方舟的 Coding Plan 为例: @@ -136,6 +140,7 @@ Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览 "thinkingEnabled": true } ``` + ## 贡献 欢迎贡献代码!以下是参与方式: diff --git a/docs/permission.md b/docs/permission.md new file mode 100644 index 00000000..91c19c6f --- /dev/null +++ b/docs/permission.md @@ -0,0 +1,101 @@ +# Deep Code 权限机制 + +Deep Code 内置了一套细粒度的权限控制机制,在 AI 助手执行工具调用(如执行 Shell 命令、读写文件、访问网络等)前,根据用户配置的策略决定是自动放行、直接拒绝、还是弹出交互式确认。 + +## 概述 + +每次 AI 助手调用工具时,系统会自动分析该操作涉及的**权限范围(Permission Scope)**,然后根据 `settings.json` 中的权限配置做出决策。对于需要用户确认的操作,会在终端中弹出交互式选择界面,用户可以选择: + +- **Yes** — 仅本次放行 +- **Yes, and always allow** — 本次放行,并将该权限范围写入项目配置文件,后续同类操作不再询问 +- **No** — 拒绝本次操作 + +## 权限范围 + +Deep Code 定义了以下 10 种权限范围,覆盖了工具调用的各类风险场景: + +| 权限范围 | 说明 | +| -------- | ---- | +| `read-in-cwd` | 读取当前工作区内的文件 | +| `read-out-cwd` | 读取当前工作区外的文件 | +| `write-in-cwd` | 在当前工作区内创建或覆写文件 | +| `write-out-cwd` | 在当前工作区外创建或覆写文件 | +| `delete-in-cwd` | 删除当前工作区内的文件 | +| `delete-out-cwd` | 删除当前工作区外的文件 | +| `query-git-log` | 查询 Git 历史(如 `git log`、`git show`、`git blame`) | +| `mutate-git-log` | 修改 Git 历史(如 `git commit`、`git rebase`、`git tag`) | +| `network` | 访问网络(如 `curl`、`npm install` 等联网操作) | +| `mcp` | 调用 MCP 外部工具 | + +此外还有一个特殊的 `unknown` 范围,当 LLM 无法准确分类命令的副作用时使用,**`unknown` 总是触发询问**。 + +## 权限配置 + +在 `~/.deepcode/settings.json`(用户级)或 `.deepcode/settings.json`(项目级)中通过 `permissions` 字段配置: + +```json +{ + "permissions": { + "allow": [], + "deny": [], + "ask": [], + "defaultMode": "allowAll" + } +} +``` + +### 配置项说明 + +| 字段 | 类型 | 说明 | +| ---- | ---- | ---- | +| `allow` | `string[]` | 始终自动放行的权限范围列表 | +| `deny` | `string[]` | 始终自动拒绝的权限范围列表 | +| `ask` | `string[]` | 始终弹出询问的权限范围列表 | +| `defaultMode` | `"allowAll"` \| `"askAll"` | 未在 `allow`/`deny`/`ask` 中明确列出的权限范围的默认处理方式。默认为 `"allowAll"` | + +### 优先级规则 + +当一个工具调用涉及多个权限范围时,决策按以下优先级进行: + +1. 若任一范围命中 `deny` → **拒绝** +2. 若任一范围命中 `ask` → **询问** +3. 若所有范围均在 `allow` 中 → **自动放行** +4. 否则 → 按 `defaultMode` 处理 + +### 示例:宽松模式(默认) + +```json +{ + "permissions": { + "defaultMode": "allowAll" + } +} +``` + +默认行为:所有操作自动放行,无需确认。 + +### 示例:严格模式 + +```json +{ + "permissions": { + "allow": ["read-in-cwd", "write-in-cwd", "query-git-log"], + "defaultMode": "askAll" + } +} +``` + +此配置的效果: +- 工作区内读写、Git 查询 → 自动放行 +- 其他操作都需要用户确认。 + + +## 持久化机制 + +当用户在权限提示中选择 "Yes, and always allow" 后,对应的权限范围会被写入当前项目的 `.deepcode/settings.json` 文件中: + +- 新增范围会追加到 `permissions.allow` 列表 +- 如果该范围之前存在于 `deny` 或 `ask` 中,会被自动移除 +- 不会重复写入已存在的范围 + +这样后续同类操作就不再询问。 diff --git a/docs/permission_en.md b/docs/permission_en.md new file mode 100644 index 00000000..dae739c0 --- /dev/null +++ b/docs/permission_en.md @@ -0,0 +1,100 @@ +# Deep Code Permission Mechanism + +Deep Code includes a fine-grained permission control mechanism. Before the AI assistant executes a tool call (such as running a shell command, reading/writing files, accessing the network, etc.), the system determines whether to auto-allow, auto-deny, or prompt for interactive confirmation based on your configured policy. + +## Overview + +Each time the AI assistant invokes a tool, the system automatically analyzes the **permission scopes** involved and makes a decision based on the permission configuration in `settings.json`. For operations requiring user confirmation, an interactive prompt appears in the terminal with the following choices: + +- **Yes** — Allow this one time only +- **Yes, and always allow** — Allow this time and persistently save the scope to the project configuration so future calls skip the prompt +- **No** — Deny this operation + +## Permission Scopes + +Deep Code defines the following 10 permission scopes, covering various risk scenarios for tool calls: + +| Permission Scope | Description | +| ---------------- | ----------- | +| `read-in-cwd` | Read files inside the current workspace | +| `read-out-cwd` | Read files outside the current workspace | +| `write-in-cwd` | Create or overwrite files inside the current workspace | +| `write-out-cwd` | Create or overwrite files outside the current workspace | +| `delete-in-cwd` | Delete files inside the current workspace | +| `delete-out-cwd` | Delete files outside the current workspace | +| `query-git-log` | Query Git history (e.g., `git log`, `git show`, `git blame`) | +| `mutate-git-log` | Mutate Git history (e.g., `git commit`, `git rebase`, `git tag`) | +| `network` | Access the network (e.g., `curl`, `npm install`) | +| `mcp` | Invoke MCP external tools | + +There is also a special `unknown` scope used when the LLM cannot classify a command's side effects — **`unknown` always triggers a prompt**. + +## Permission Configuration + +Configure permissions in `~/.deepcode/settings.json` (user-level) or `.deepcode/settings.json` (project-level) via the `permissions` field: + +```json +{ + "permissions": { + "allow": [], + "deny": [], + "ask": [], + "defaultMode": "allowAll" + } +} +``` + +### Configuration Fields + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `allow` | `string[]` | Permission scopes that are always auto-allowed | +| `deny` | `string[]` | Permission scopes that are always auto-denied | +| `ask` | `string[]` | Permission scopes that always trigger a confirmation prompt | +| `defaultMode` | `"allowAll"` \| `"askAll"` | Default behavior for scopes not explicitly listed in `allow`/`deny`/`ask`. Defaults to `"allowAll"` | + +### Priority Rules + +When a tool call involves multiple permission scopes, the decision follows this priority: + +1. If any scope matches `deny` → **Deny** +2. If any scope matches `ask` → **Prompt** +3. If all scopes are in `allow` → **Auto-allow** +4. Otherwise → use `defaultMode` + +### Example: Relaxed Mode (default) + +```json +{ + "permissions": { + "defaultMode": "allowAll" + } +} +``` + +Default behavior: all operations are auto-allowed with no confirmation required. + +### Example: Strict Mode + +```json +{ + "permissions": { + "allow": ["read-in-cwd", "write-in-cwd", "query-git-log"], + "defaultMode": "askAll" + } +} +``` + +With this configuration: +- Reading/writing inside the workspace and querying Git history → auto-allowed +- All other operations → require user confirmation + +## Persistence + +When you select "Yes, and always allow" in a permission prompt, the corresponding scope is written to the project's `.deepcode/settings.json`: + +- The scope is appended to the `permissions.allow` list +- If the scope was previously in `deny` or `ask`, it is automatically removed +- Duplicate scopes are not written again + +This means subsequent calls involving the same scope will no longer prompt for confirmation. From 7c95312f68623c9da6ce1c865f2dd996d16b2cf5 Mon Sep 17 00:00:00 2001 From: dengm Date: Mon, 25 May 2026 11:17:12 +0800 Subject: [PATCH 068/212] =?UTF-8?q?fix:=20improve=20table=20column=20width?= =?UTF-8?q?=20allocation=20=E2=80=94=20use=20natural=20widths=20and=20grow?= =?UTF-8?q?=20to=20fill=20terminal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the aggressive maxLine/1.5 ideal-width heuristic with full natural widths. When the total fits within the available terminal width (defaulting to 120 cols), distribute slack proportionally to content columns instead of leaving them cramped. Detect narrow label columns by actual content width (≤8 chars) rather than hardcoded position ([0, 1, -2, -1]). When compression is necessary, start from per-column minimums (longest word) and share the remaining budget proportionally based on each column's deficit. This fixes the "tables too narrow and too tall" issue reported on PR #115 where every column was forced to ~67% of its natural width regardless of available screen real estate. --- src/ui/components/MessageView/markdown.ts | 84 +++++++++++++++-------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/src/ui/components/MessageView/markdown.ts b/src/ui/components/MessageView/markdown.ts index 8c865343..f5b72bc3 100644 --- a/src/ui/components/MessageView/markdown.ts +++ b/src/ui/components/MessageView/markdown.ts @@ -231,42 +231,68 @@ function renderTableBorder(rows: string[][], maxWidth?: number): string { const colCount = rows[0].length; const calcW = (cs: number[]) => cs.reduce((a, b) => a + b + 2, 0) + cs.length + 1; - // Ideal widths — longest word / 1.5 so cells can wrap in 2-3 lines - const ideal: number[] = Array.from({ length: colCount }, (_, i) => { + // Natural width per column: longest line + cell padding + const natural: number[] = Array.from({ length: colCount }, (_, i) => { + const texts = rows.map((r) => r[i] ?? ""); + const maxLine = Math.max(4, ...texts.map((t) => visualWidth(t))); + return maxLine + 2; + }); + + // Minimum width per column: longest word + padding (can't go below this) + const minWidths: number[] = Array.from({ length: colCount }, (_, i) => { const texts = rows.map((r) => r[i] ?? ""); - const maxLine = Math.max(...texts.map((t) => visualWidth(t))); const words = texts.flatMap((t) => t.split(/\s+/)); const maxWord = Math.max(4, ...words.map((w) => visualWidth(w))); - return Math.max(maxWord + 2, Math.ceil(maxLine / 1.5)); + return maxWord + 2; }); - const colWidths = [...ideal]; - - // Shrink to fit terminal width - if (maxWidth != null && calcW(colWidths) > maxWidth) { - const narrow = new Set([0, 1, colCount - 2, colCount - 1]); // #, status, count, date - const MIN_NARROW = 6; - const MIN_CONTENT = 12; - const contentCols = Array.from({ length: colCount }, (_, i) => i).filter((i) => !narrow.has(i)); - - // Cap narrow columns first - for (const ci of narrow) colWidths[ci] = Math.min(colWidths[ci], MIN_NARROW); - - // Shrink until we fit - while (calcW(colWidths) > maxWidth) { - // Try narrow columns first - let shrunk = false; - for (const ci of narrow) { - if (colWidths[ci] > 4 && calcW(colWidths) > maxWidth) { - colWidths[ci]--; - shrunk = true; + let colWidths: number[]; + const totalNatural = calcW(natural); + const totalMin = calcW(minWidths); + + const effectiveMax = maxWidth ?? 120; // default to a generous terminal width + + if (totalNatural <= effectiveMax) { + // Content fits comfortably — use natural widths and grow to fill available space + colWidths = [...natural]; + const slack = effectiveMax - totalNatural; + if (slack > 0) { + // Distribute slack proportionally to content columns (skip tiny label columns) + const isLabel = colWidths.map((w) => w <= 8); + const candidates = colWidths.map((w, i) => (isLabel[i] ? 0 : w)); + const totalWeight = candidates.reduce((a, b) => a + b, 0); + if (totalWeight > 0) { + for (let ci = 0; ci < colCount; ci++) { + if (candidates[ci] > 0) { + colWidths[ci] += Math.floor((slack * candidates[ci]) / totalWeight); + } } } - if (shrunk) continue; - // Then content columns - const widest = contentCols.reduce((a, b) => (colWidths[a] > colWidths[b] ? a : b), contentCols[0]); - if (colWidths[widest] > MIN_CONTENT) colWidths[widest]--; - else break; + } + } else if (totalMin >= effectiveMax) { + // Even minimums don't fit — use mins and accept truncation + colWidths = [...minWidths]; + } else { + // Need to compress — start from mins, share remaining budget proportionally + const budget = effectiveMax - totalMin; + const deficits = natural.map((n, i) => Math.max(0, n - minWidths[i])); + const totalDeficit = deficits.reduce((a, b) => a + b, 0); + colWidths = [...minWidths]; + if (totalDeficit > 0) { + for (let ci = 0; ci < colCount; ci++) { + colWidths[ci] += Math.floor((budget * deficits[ci]) / totalDeficit); + } + } + // Distribute any leftover due to flooring + let used = calcW(colWidths); + const deficitByIdx = colWidths.map((w, i) => ({ i, gap: natural[i] - w })); + deficitByIdx.sort((a, b) => b.gap - a.gap); + for (const { i } of deficitByIdx) { + if (used >= effectiveMax) break; + if (colWidths[i] < natural[i]) { + colWidths[i]++; + used = calcW(colWidths); + } } } From abed1495821b1c9d82f870d05e62dd83afffbaa6 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 11:47:09 +0800 Subject: [PATCH 069/212] =?UTF-8?q?feat(ui):=20=E5=A2=9E=E5=8A=A0=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E5=88=A0=E9=99=A4=E5=8F=8A=E7=9B=B8=E5=85=B3UI?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 handleDeleteSession 方法,支持删除会话并更新会话列表 - 删除当前激活会话时,清除屏幕、重置UI状态并显示欢迎界面 - 删除按钮调用封装的删除处理函数,统一逻辑 - 优化 SessionList 中搜索逻辑,调整删除和退格键处理 - 移除 SessionList 文件内重复的 truncate 函数实现 --- src/ui/App.tsx | 181 ++++++++++++++++++++++------------------ src/ui/AppContainer.tsx | 2 +- src/ui/SessionList.tsx | 19 +---- src/ui/constants.ts | 3 + src/ui/utils/index.ts | 24 ++++++ 5 files changed, 132 insertions(+), 97 deletions(-) create mode 100644 src/ui/utils/index.ts diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 38799153..71e9ca3e 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -43,6 +43,8 @@ import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPromp import { buildExitSummaryText } from "./exitSummary"; import { RawMode, useRawModeContext } from "./contexts"; import { renderMessageToStdout } from "./components/MessageView/utils"; +import { renderRawModeMessages } from "./utils"; +import { ANSI_CLEAR_SCREEN } from "./constants"; const DEFAULT_MODEL = "deepseek-v4-pro"; const DEFAULT_BASE_URL = "https://api.deepseek.com"; @@ -55,7 +57,7 @@ type AppProps = { onRestart?: () => void; }; -export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement { +function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement { const { exit } = useApp(); const { stdout, write } = useStdout(); const { columns, rows } = useWindowSize(); @@ -142,6 +144,33 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. }); }, [projectRoot]); + /** + * Navigate to a sub-view. + */ + const navigateToSubView = useCallback((targetView: View) => { + setShowWelcome(false); + setView(targetView); + }, []); + + /** + * Reset the static view to the welcome screen. + */ + const resetStaticView = useCallback( + (loadedMessages: SessionMessage[], options?: { clearScreen?: boolean }) => { + if (options?.clearScreen) { + process.stdout.write(ANSI_CLEAR_SCREEN); + } + setMessages([]); + setWelcomeNonce((n) => n + 1); + navigateToSubView("chat"); + setTimeout(() => { + setMessages(loadedMessages); + setShowWelcome(true); + }, 0); + }, + [navigateToSubView] + ); + useEffect(() => { if (!busy) { return; @@ -170,6 +199,26 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. [sessionManager] ); + /** + * Reset the app to the welcome screen. + */ + const resetToWelcome = useCallback(async () => { + writeRef.current(ANSI_CLEAR_SCREEN); + sessionManager.setActiveSessionId(null); + setStatusLine(""); + setErrorLine(null); + setRunningProcesses(null); + setActiveStatus(null); + setActiveAskPermissions(undefined); + setPendingPermissionReply(null); + setDismissedQuestionIds(new Set()); + resetStaticView([]); + await refreshSkills(); + }, [sessionManager, resetStaticView, refreshSkills]); + + /** + * Refresh the list of sessions. + */ useEffect(() => { refreshSessionsList(); void refreshSkills(); @@ -182,11 +231,17 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. createOpenAIClient(projectRoot); }, [projectRoot]); + /** + * Initialize MCP servers. + */ useLayoutEffect(() => { const settings = resolveCurrentSettings(projectRoot); void sessionManager.initMcpServers(settings.mcpServers); }, [projectRoot, sessionManager]); + /** + * Dispose the session manager on unmount. + */ useEffect(() => { return () => { sessionManager.dispose(); @@ -216,33 +271,19 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. if (onRestart) { onRestart(); } else { - writeRef.current("\u001B[2J\u001B[3J\u001B[H"); - sessionManager.setActiveSessionId(null); - setMessages([]); - setStatusLine(""); - setErrorLine(null); - setRunningProcesses(null); - setActiveStatus(null); - setActiveAskPermissions(undefined); - setPendingPermissionReply(null); - setDismissedQuestionIds(new Set()); - setShowWelcome(true); - setWelcomeNonce((n) => n + 1); - await refreshSkills(); + await resetToWelcome(); refreshSessionsList(); } return; } if (submission.command === "resume") { - setShowWelcome(false); refreshSessionsList(); - setView("session-list"); + navigateToSubView("session-list"); return; } if (submission.command === "continue" && isCurrentSessionEmpty(sessionManager)) { - setShowWelcome(false); refreshSessionsList(); - setView("session-list"); + navigateToSubView("session-list"); return; } if (submission.command === "undo") { @@ -251,15 +292,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setErrorLine("No active session to undo."); return; } - setShowWelcome(false); setUndoTargets(sessionManager.listUndoTargets(activeSessionId)); - setView("undo"); + navigateToSubView("undo"); return; } if (submission.command === "mcp") { - setShowWelcome(false); setMcpStatuses(sessionManager.getMcpStatus()); - setView("mcp-status"); + navigateToSubView("mcp-status"); return; } @@ -311,7 +350,16 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setRunningProcesses(null); } }, - [exit, onRestart, pendingPermissionReply, sessionManager, refreshSkills, refreshSessionsList] + [ + sessionManager, + pendingPermissionReply, + exit, + onRestart, + refreshSkills, + refreshSessionsList, + navigateToSubView, + resetToWelcome, + ] ); const handleInterrupt = useCallback(() => { @@ -384,16 +432,9 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. const reloadActiveSessionView = useCallback( (sessionId: string): void => { - process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); - setMessages([]); - setShowWelcome(false); - setWelcomeNonce((n) => n + 1); - setTimeout(() => { - setMessages(loadVisibleMessages(sessionManager, sessionId)); - setShowWelcome(true); - }, 0); + resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true }); }, - [sessionManager] + [resetStaticView, sessionManager] ); useEffect(() => { @@ -411,21 +452,9 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. const handleSelectSession = useCallback( async (sessionId: string) => { - const currentSessionId = sessionManager.getActiveSessionId(); - if (currentSessionId !== sessionId) { - process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); - } sessionManager.setActiveSessionId(sessionId); // Clear first so resets its index to 0. - setMessages([]); - setShowWelcome(false); - setWelcomeNonce((n) => n + 1); - setView("chat"); - // Load messages after the reset so all static items are rendered. - setTimeout(() => { - setMessages(loadVisibleMessages(sessionManager, sessionId)); - setShowWelcome(true); - }, 0); + resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true }); const session = sessionManager.getSession(sessionId); setStatusLine(session ? buildStatusLine(session) : ""); setRunningProcesses(session?.processes ?? null); @@ -436,7 +465,26 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. } await refreshSkills(sessionId); }, - [pendingPermissionReply, sessionManager, refreshSkills] + [sessionManager, resetStaticView, pendingPermissionReply, refreshSkills] + ); + + const handleDeleteSession = useCallback( + async (id: string): Promise => { + const isActiveSession = sessionManager.getActiveSessionId() === id; + + // If the deleted session is the active one, clear the active session first + if (isActiveSession) { + sessionManager.setActiveSessionId(null); + } + + sessionManager.deleteSession(id); + refreshSessionsList(); + + if (isActiveSession) { + await resetToWelcome(); + } + }, + [sessionManager, refreshSessionsList, resetToWelcome] ); const handleUndoRestore = useCallback( @@ -487,25 +535,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. setShowWelcome(false); setMessages([]); // Clear screen to remove stale formatted text. - process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); + process.stdout.write(ANSI_CLEAR_SCREEN); setTimeout(() => { if (nextMode === RawMode.Raw) { // Write all messages directly to stdout for raw scrollback mode. const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : []; - for (const msg of allMessages) { - process.stdout.write("\n"); - process.stdout.write(renderMessageToStdout(msg, nextMode) + "\n\n"); - } - if (allMessages.length > 0) { - process.stdout.write("\n\n"); - process.stdout.write(chalk.dim("Press ESC to exit raw mode")); - } else { - process.stdout.write("\n"); - process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)")); - process.stdout.write("\n\n"); - process.stdout.write(chalk.dim("Press ESC to exit raw mode")); - } + renderRawModeMessages(allMessages, nextMode); } else if (activeSessionId) { // Switch to chat view to render messages. handleSelectSession(activeSessionId); @@ -538,22 +574,10 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. if (mode === RawMode.Raw) { // In raw mode, re-render all messages directly to stdout at the new width. // Use process.stdout.write instead of writeRef to avoid Ink interference. - process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); + process.stdout.write(ANSI_CLEAR_SCREEN); const activeSessionId = sessionManager.getActiveSessionId(); const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : []; - for (const msg of allMessages) { - process.stdout.write("\n"); - process.stdout.write(renderMessageToStdout(msg, mode) + "\n\n"); - } - if (allMessages.length > 0) { - process.stdout.write("\n\n"); - process.stdout.write(chalk.dim("Press ESC to exit raw mode")); - } else { - process.stdout.write("\n"); - process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)")); - process.stdout.write("\n\n"); - process.stdout.write(chalk.dim("Press ESC to exit raw mode")); - } + renderRawModeMessages(allMessages, mode); return; } @@ -719,12 +743,7 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. onSelect={(id) => void handleSelectSession(id)} onCancel={() => setView("chat")} onDelete={(id) => { - // If the deleted session is the active one, clear it - if (sessionManager.getActiveSessionId() === id) { - sessionManager.setActiveSessionId(null); - } - sessionManager.deleteSession(id); - refreshSessionsList(); + void handleDeleteSession(id); }} /> ) : view === "undo" ? ( @@ -784,6 +803,8 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React. ); } +export default App; + function isCollapsedThinking(message: SessionMessage, expandedId: string | null): boolean { if (message.role !== "assistant") { return false; diff --git a/src/ui/AppContainer.tsx b/src/ui/AppContainer.tsx index e437b44a..c8b31773 100644 --- a/src/ui/AppContainer.tsx +++ b/src/ui/AppContainer.tsx @@ -1,6 +1,6 @@ import React from "react"; import { AppContext } from "./contexts"; -import { App } from "./App"; +import App from "./App"; import { RawModeProvider } from "./contexts/RawModeContext"; const AppContainer: React.FC<{ diff --git a/src/ui/SessionList.tsx b/src/ui/SessionList.tsx index 7d7e04e4..82ca7972 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/SessionList.tsx @@ -1,6 +1,7 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; import type { SessionEntry, SessionStatus } from "../session"; +import { truncate } from "./components/MessageView/utils"; type Props = { sessions: SessionEntry[]; @@ -113,17 +114,10 @@ export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): return; } - // Backspace: remove last search character - if (key.backspace) { - if (searchQuery) { - handleBackspace(); - return; - } - } - // Delete key: remove search character, or start delete confirmation - if (key.delete) { + if (key.delete || key.backspace) { if (searchQuery) { + // remove last search character handleBackspace(); return; } @@ -342,10 +336,3 @@ export function formatSessionStatus(status: SessionStatus): string { return status; } } - -function truncate(value: string, max: number): string { - if (value.length <= max) { - return value; - } - return `${value.slice(0, max)}…`; -} diff --git a/src/ui/constants.ts b/src/ui/constants.ts index 7c74597b..43372f80 100644 --- a/src/ui/constants.ts +++ b/src/ui/constants.ts @@ -2,3 +2,6 @@ /** Separator used when rendering command arguments inline (e.g., `arg1 | arg2 | arg3`). */ export const ARGS_SEPARATOR = " | "; + +/** ANSI escape code to clear the screen. */ +export const ANSI_CLEAR_SCREEN = "\u001B[2J\u001B[3J\u001B[H"; diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts new file mode 100644 index 00000000..4b498a06 --- /dev/null +++ b/src/ui/utils/index.ts @@ -0,0 +1,24 @@ +import chalk from "chalk"; +import type { SessionMessage } from "../../session"; +import { renderMessageToStdout } from "../components/MessageView/utils"; +import type { RawMode } from "../contexts"; + +/** + * Render all messages directly to stdout for Raw mode display. + * Writes each message followed by the "Press ESC to exit raw mode" footer. + */ +export function renderRawModeMessages(allMessages: SessionMessage[], mode: string | RawMode): void { + for (const msg of allMessages) { + process.stdout.write("\n"); + process.stdout.write(renderMessageToStdout(msg, mode as RawMode) + "\n\n"); + } + if (allMessages.length > 0) { + process.stdout.write("\n\n"); + process.stdout.write(chalk.dim("Press ESC to exit raw mode")); + } else { + process.stdout.write("\n"); + process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)")); + process.stdout.write("\n\n"); + process.stdout.write(chalk.dim("Press ESC to exit raw mode")); + } +} From 679eb003515d15a9bf2c6f3d147650cd5d768fb9 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 25 May 2026 14:45:39 +0800 Subject: [PATCH 070/212] feat: enhance markdown table rendering with CJK support and improved width handling --- src/tests/markdown.test.ts | 55 ++++++++++++++++++++++- src/ui/components/MessageView/index.tsx | 10 +++-- src/ui/components/MessageView/markdown.ts | 54 ++++++++++++---------- 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/src/tests/markdown.test.ts b/src/tests/markdown.test.ts index a0127fcb..bc5d33cb 100644 --- a/src/tests/markdown.test.ts +++ b/src/tests/markdown.test.ts @@ -1,11 +1,26 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { renderMarkdown } from "../ui"; +import { renderMarkdown, renderMarkdownSegments } from "../ui"; function stripAnsi(text: string): string { return text.replace(/\[[0-9;]*m/g, ""); } +function visualWidth(text: string): number { + let width = 0; + for (const ch of text) { + const code = ch.codePointAt(0) ?? 0; + width += + ch.length >= 2 || + (code >= 0x2e80 && code <= 0xa4cf) || + (code >= 0xf900 && code <= 0xfaff) || + (code >= 0xff00 && code <= 0xffe6) + ? 2 + : 1; + } + return width; +} + test("renderMarkdown returns empty string for empty input", () => { assert.equal(renderMarkdown(""), ""); }); @@ -38,3 +53,41 @@ test("renderMarkdown handles plain text unchanged in stripped form", () => { const result = stripAnsi(renderMarkdown(text)); assert.equal(result, text); }); + +test("renderMarkdownSegments renders CJK table cells within the requested width", () => { + const table = [ + "| 编号 | 状态 | 任务 | 备注 |", + "|---|---|---|---|", + "| 1 | ✅ | 写代码 | 这是一个很长很长的中文备注用于验证表格在终端宽度不足时是否能够自动换行而不是溢出 |", + ].join("\n"); + + const segment = renderMarkdownSegments(table, 60).find((item) => item.kind === "table"); + assert.ok(segment); + const lines = stripAnsi(segment.body).split("\n"); + assert.equal(lines[0].startsWith("┌"), true); + assert.equal(lines.at(-1)?.startsWith("└"), true); + assert.equal( + lines.every((line) => visualWidth(line) <= 60), + true + ); + assert.equal(lines.length > 4, true); +}); + +test("renderMarkdown preserves empty table cells", () => { + const result = stripAnsi(renderMarkdown("| A | B | C |\n|---|---|---|\n|x||z|", 80)); + const bodyRow = result.split("\n").find((line) => line.includes("x") && line.includes("z")); + assert.ok(bodyRow); + assert.equal((bodyRow.match(/│/g) ?? []).length, 4); +}); + +test("renderMarkdown keeps text separated from rendered table blocks", () => { + const result = stripAnsi(renderMarkdown("Before\n| A | B |\n|---|---|\n| 1 | 2 |\nAfter", 40)); + assert.equal(result.includes("Before\n┌"), true); + assert.equal(result.includes("┘\nAfter"), true); +}); + +test("renderMarkdown does not render tables inside code fences", () => { + const result = stripAnsi(renderMarkdown("```md\n| A | B |\n|---|---|\n| 1 | 2 |\n```", 40)); + assert.equal(result.includes("| A | B |"), true); + assert.equal(result.includes("┌"), false); +}); diff --git a/src/ui/components/MessageView/index.tsx b/src/ui/components/MessageView/index.tsx index 093dbc27..9c315516 100644 --- a/src/ui/components/MessageView/index.tsx +++ b/src/ui/components/MessageView/index.tsx @@ -71,9 +71,13 @@ export function MessageView({ message, collapsed, width = 80 }: MessageViewProps ? renderMarkdownSegments(content, Math.max(20, contentWidth - 4)).map((seg, i) => { if (seg.kind === "table") { return ( - - {seg.body} - + + {seg.body.split("\n").map((line, lineIndex) => ( + + {line} + + ))} + ); } return {seg.body}; diff --git a/src/ui/components/MessageView/markdown.ts b/src/ui/components/MessageView/markdown.ts index f5b72bc3..3ebb58ba 100644 --- a/src/ui/components/MessageView/markdown.ts +++ b/src/ui/components/MessageView/markdown.ts @@ -18,7 +18,11 @@ export type MarkdownSegment = export function renderMarkdown(text: string, maxWidth?: number): string { return renderMarkdownSegments(text, maxWidth) .map((s) => s.body) - .join(""); + .reduce((out, body) => { + if (!out) return body; + if (!body) return out; + return out.endsWith("\n") || body.startsWith("\n") ? out + body : `${out}\n${body}`; + }, ""); } /** Render markdown, returning typed segments so the caller can choose the @@ -128,6 +132,12 @@ function splitTableBlocks(text: string): TableBlock[] { }; const sepRe = /^\|?\s*:?[-]{3,}:?\s*(\|\s*:?[-]{3,}:?\s*)*\|?\s*$/; + const parseRow = (row: string) => { + let body = row.trim(); + if (body.startsWith("|")) body = body.slice(1); + if (body.endsWith("|")) body = body.slice(0, -1); + return body.split("|").map((s) => s.trim()); + }; for (let i = 0; i < lines.length; i++) { const line = lines[i]; @@ -143,22 +153,12 @@ function splitTableBlocks(text: string): TableBlock[] { if (isHeader && !inTable) { flushText(); inTable = true; - tableRows = [ - trimmed - .split("|") - .filter(Boolean) - .map((s) => s.trim()), - ]; + tableRows = [parseRow(trimmed)]; continue; } if (isRow && inTable) { - tableRows.push( - trimmed - .split("|") - .filter(Boolean) - .map((s) => s.trim()) - ); + tableRows.push(parseRow(trimmed)); continue; } @@ -229,21 +229,26 @@ function renderTableBorder(rows: string[][], maxWidth?: number): string { if (rows.length === 0) return ""; const colCount = rows[0].length; + const normalizedRows = rows.map((row) => + Array.from({ length: colCount }, (_, i) => { + return row[i] ?? ""; + }) + ); const calcW = (cs: number[]) => cs.reduce((a, b) => a + b + 2, 0) + cs.length + 1; - // Natural width per column: longest line + cell padding + // Natural width per column, measured as terminal cells rather than UTF-16 units. const natural: number[] = Array.from({ length: colCount }, (_, i) => { - const texts = rows.map((r) => r[i] ?? ""); + const texts = normalizedRows.map((r) => r[i] ?? ""); const maxLine = Math.max(4, ...texts.map((t) => visualWidth(t))); - return maxLine + 2; + return maxLine; }); - // Minimum width per column: longest word + padding (can't go below this) + // Keep minimums small so long CJK text or unbroken tokens can wrap by character. const minWidths: number[] = Array.from({ length: colCount }, (_, i) => { - const texts = rows.map((r) => r[i] ?? ""); - const words = texts.flatMap((t) => t.split(/\s+/)); - const maxWord = Math.max(4, ...words.map((w) => visualWidth(w))); - return maxWord + 2; + const headerWidth = visualWidth(normalizedRows[0]?.[i] ?? ""); + const labelColumn = natural[i] <= 12; + const minReadable = labelColumn ? natural[i] : Math.max(4, Math.min(headerWidth, 12)); + return Math.min(natural[i], minReadable); }); let colWidths: number[]; @@ -270,8 +275,11 @@ function renderTableBorder(rows: string[][], maxWidth?: number): string { } } } else if (totalMin >= effectiveMax) { - // Even minimums don't fit — use mins and accept truncation colWidths = [...minWidths]; + while (calcW(colWidths) > effectiveMax && colWidths.some((w) => w > 1)) { + const widest = colWidths.reduce((maxIdx, width, idx) => (width > colWidths[maxIdx] ? idx : maxIdx), 0); + colWidths[widest]--; + } } else { // Need to compress — start from mins, share remaining budget proportionally const budget = effectiveMax - totalMin; @@ -327,7 +335,7 @@ function renderTableBorder(rows: string[][], maxWidth?: number): string { return lines.length > 0 ? lines : [""]; }; - const wrapped = rows.map((r) => r.map((c, ci) => wrapCell(c, colWidths[ci]))); + const wrapped = normalizedRows.map((r) => r.map((c, ci) => wrapCell(c, colWidths[ci]))); const heights = wrapped.map((wr) => Math.max(1, ...wr.map((lines) => lines.length))); const pad = (s: string, w: number) => s + " ".repeat(Math.max(0, w - visualWidth(s))); From 670b118cd64c28a01c0a1ae985279e0807300e2d Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 15:10:37 +0800 Subject: [PATCH 071/212] =?UTF-8?q?fix(session):=20=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=8B=92=E7=BB=9D=E7=8A=B6=E6=80=81=E4=B8=8E?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 permission_denied 会话状态表示权限被拒绝 - 添加 denySessionPermission 方法以更新会话状态为拒绝并设置失败原因 - 在权限拒绝时清除提示草稿并调用拒绝权限处理逻辑 - 中断会话时清除提示草稿以防止残留输入 - 会话列表中新增 permission_denied 状态对应的 UI 状态映射为 denied --- src/session.ts | 17 ++++++++++++++++- src/ui/App.tsx | 3 +++ src/ui/SessionList.tsx | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index c81ae4a5..4b34f209 100644 --- a/src/session.ts +++ b/src/session.ts @@ -159,7 +159,8 @@ export type SessionStatus = | "waiting_for_user" | "completed" | "interrupted" - | "ask_permission"; + | "ask_permission" + | "permission_denied"; export type ModelUsage = { prompt_tokens: number; @@ -1532,6 +1533,20 @@ ${skillMd} return !this.sessionControllers.has(sessionId); } + /** + * Mark a session's permission as denied by the user. + * Updates the session entry status and failReason so the denial is visible in the session list. + */ + denySessionPermission(sessionId: string, reason?: string): void { + const now = new Date().toISOString(); + this.updateSessionEntry(sessionId, (entry) => ({ + ...entry, + status: "permission_denied", + failReason: reason ?? "Permission denied by user", + updateTime: now, + })); + } + adjustActiveBashTimeout(deltaMs: number): BashTimeoutAdjustment | null { const sessionId = this.activeSessionId; if (!sessionId || !Number.isFinite(deltaMs)) { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 71e9ca3e..ae94fa06 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -669,6 +669,8 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl alwaysAllows: result.alwaysAllows, }); setStatusLine("Permission denied. Add a reply, then press Enter to continue."); + setPromptDraft(null); + sessionManager.denySessionPermission(sessionId); return; } void handlePrompt({ @@ -686,6 +688,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl sessionManager.interruptActiveSession(); setActiveStatus("interrupted"); setActiveAskPermissions(undefined); + setPromptDraft(null); refreshSessionsList(); }, [refreshSessionsList, sessionManager]); diff --git a/src/ui/SessionList.tsx b/src/ui/SessionList.tsx index 82ca7972..2d83b847 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/SessionList.tsx @@ -332,6 +332,10 @@ export function formatSessionStatus(status: SessionStatus): string { return "failed"; case "interrupted": return "stopped"; + case "ask_permission": + return "waiting"; + case "permission_denied": + return "denied"; default: return status; } From f1ecc26f4cf9f29c820138d005cfa604a76ffce4 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 25 May 2026 16:03:22 +0800 Subject: [PATCH 072/212] feat: update the MCP spawn to avoid DEP0190 problem --- src/mcp/mcp-client.ts | 61 +++++++++++++++++++++++++----------- src/tests/mcp-client.test.ts | 34 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 src/tests/mcp-client.test.ts diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index b859bf5b..26a7a321 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -1,6 +1,5 @@ import { spawn, type ChildProcess } from "child_process"; import { createInterface, type Interface } from "readline"; -import * as os from "os"; import * as path from "path"; import { killProcessTree } from "../common/process-tree"; @@ -97,6 +96,13 @@ type ReadResourceResult = { export type McpNotificationHandler = (method: string, params?: Record) => void; +export type McpSpawnSpec = { + command: string; + args: string[]; + shell: boolean; + windowsHide?: boolean; +}; + export class McpClient { private process: ChildProcess | null = null; private reader: Interface | null = null; @@ -130,25 +136,14 @@ export class McpClient { ...this.env, }; const args = this.withNpxYesArg(this.command, this.args); + const spawnSpec = createMcpSpawnSpec(this.command, args); - const isWindows = os.platform() === "win32"; - - if (isWindows) { - // On Windows, shell: true lets cmd.exe resolve the command via - // PATHEXT (npx → npx.cmd, etc.) without blindly appending .cmd, - // which would break absolute paths like process.execPath. - this.process = spawn(this.command, args, { - stdio: ["pipe", "pipe", "pipe"], - env: childEnv, - shell: true, - windowsHide: true, - }); - } else { - this.process = spawn(this.command, args, { - stdio: ["pipe", "pipe", "pipe"], - env: childEnv, - }); - } + this.process = spawn(spawnSpec.command, spawnSpec.args, { + stdio: ["pipe", "pipe", "pipe"], + env: childEnv, + shell: spawnSpec.shell, + windowsHide: spawnSpec.windowsHide, + }); let resolved = false; const safeReject = (err: Error) => { @@ -421,3 +416,31 @@ export class McpClient { return new Error(stderr ? `${message}. stderr: ${stderr}` : message); } } + +export function createMcpSpawnSpec( + command: string, + args: string[], + platform: NodeJS.Platform = process.platform +): McpSpawnSpec { + if (platform === "win32") { + return { + // On Windows, shell: true lets cmd.exe resolve the command via PATHEXT + // (npx -> npx.cmd, etc.). Pass one quoted command line with no spawn + // args to avoid Node 24 DEP0190. + command: [command, ...args].map(quoteWindowsShellArg).join(" "), + args: [], + shell: true, + windowsHide: true, + }; + } + + return { + command, + args, + shell: false, + }; +} + +function quoteWindowsShellArg(arg: string): string { + return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, "$&$&")}"`; +} diff --git a/src/tests/mcp-client.test.ts b/src/tests/mcp-client.test.ts new file mode 100644 index 00000000..e161aad3 --- /dev/null +++ b/src/tests/mcp-client.test.ts @@ -0,0 +1,34 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { createMcpSpawnSpec } from "../mcp/mcp-client"; + +test("createMcpSpawnSpec keeps non-Windows MCP launches shell-free", () => { + assert.deepEqual(createMcpSpawnSpec("npx", ["-y", "@playwright/mcp@latest"], "darwin"), { + command: "npx", + args: ["-y", "@playwright/mcp@latest"], + shell: false, + }); +}); + +test("createMcpSpawnSpec avoids Windows shell args for Node 24", () => { + assert.deepEqual(createMcpSpawnSpec("npx", ["-y", "@playwright/mcp@latest"], "win32"), { + command: '"npx" "-y" "@playwright/mcp@latest"', + args: [], + shell: true, + windowsHide: true, + }); +}); + +test("createMcpSpawnSpec quotes Windows command paths and arguments", () => { + const spec = createMcpSpawnSpec( + String.raw`C:\Program Files\nodejs\node.exe`, + [String.raw`C:\tmp\mcp server.cjs`, 'a "quoted" value'], + "win32" + ); + + assert.equal( + spec.command, + String.raw`"C:\Program Files\nodejs\node.exe" "C:\tmp\mcp server.cjs" "a \"quoted\" value"` + ); + assert.deepEqual(spec.args, []); +}); From a1b31c635263d22c486559f2c029242d51e35462 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 25 May 2026 16:15:14 +0800 Subject: [PATCH 073/212] feat(session): Add support for permission_denied status --- src/session.ts | 3 ++- src/tests/session.test.ts | 51 +++++++++++++++++++++++++++++++++++ src/tests/sessionList.test.ts | 2 ++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index 4b34f209..a9fc39e8 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2730,7 +2730,8 @@ ${skillMd} status === "waiting_for_user" || status === "completed" || status === "interrupted" || - status === "ask_permission" + status === "ask_permission" || + status === "permission_denied" ) { return status; } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index e0a863e0..95de8e35 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1313,6 +1313,57 @@ test("activateSession pauses for permission when a tool call requires ask", asyn ); }); +test("SessionManager preserves permission_denied status when sessions are reloaded", async () => { + const workspace = createTempDir("deepcode-permission-denied-workspace-"); + const home = createTempDir("deepcode-permission-denied-home-"); + setHomeDir(home); + + const permissions = { + allow: [], + deny: [], + ask: [], + defaultMode: "askAll" as const, + }; + const manager = createPermissionSessionManager( + workspace, + [ + { + choices: [ + { + message: { + content: "", + tool_calls: [ + { + id: "call-bash", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ + command: "rg TODO src", + description: "Search TODO markers", + sideEffects: ["read-in-cwd"], + }), + }, + }, + ], + }, + }, + ], + }, + ], + permissions + ); + + const sessionId = await manager.createSession({ text: "search todos" }); + manager.denySessionPermission(sessionId); + + const reloadedManager = createPermissionSessionManager(workspace, [], permissions); + const reloadedSession = reloadedManager.getSession(sessionId); + + assert.equal(reloadedSession?.status, "permission_denied"); + assert.equal(reloadedSession?.failReason, "Permission denied by user"); +}); + test("replySession applies permission replies, runs pending tools, and stores always allow scopes", async () => { const workspace = createTempDir("deepcode-permission-allow-workspace-"); const home = createTempDir("deepcode-permission-allow-home-"); diff --git a/src/tests/sessionList.test.ts b/src/tests/sessionList.test.ts index 3dfda332..6fe41c70 100644 --- a/src/tests/sessionList.test.ts +++ b/src/tests/sessionList.test.ts @@ -18,6 +18,8 @@ test("formatSessionStatus maps status values to display labels", () => { assert.equal(formatSessionStatus("waiting_for_user"), "waiting"); assert.equal(formatSessionStatus("failed"), "failed"); assert.equal(formatSessionStatus("interrupted"), "stopped"); + assert.equal(formatSessionStatus("ask_permission"), "waiting"); + assert.equal(formatSessionStatus("permission_denied"), "denied"); assert.equal(formatSessionStatus("unknown_status" as any), "unknown_status"); }); From 0d8b4838b35b4582e3d203de07d5e0b1d7c14465 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 16:27:20 +0800 Subject: [PATCH 074/212] =?UTF-8?q?refactor(ui):=20=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E5=B9=B6=E9=87=8D=E6=9E=84=E8=AE=BE=E7=BD=AE=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0=E8=87=B3utils=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将App.tsx中的设置读写及相关辅助函数移除 - 在ui/utils/index.ts新增对应工具函数实现,包括读取写入用户及项目设置 - 将DEFAULT_MODEL和DEFAULT_BASE_URL常量移至constants.ts统一管理 - 优化导入路径,修正组件和工具的引用路径 - 修改RawModeContext和openai-client模块中依赖import路径 - 统一和调整DropdownMenu相关组件的导入路径 - 使代码结构更清晰,职责划分更明确,提高维护性 --- src/common/openai-client.ts | 2 +- src/tests/dropdownMenu.test.ts | 2 +- src/ui/App.tsx | 190 ++---------------- src/ui/AppContainer.tsx | 2 +- .../DropdownMenu/index.tsx} | 0 src/ui/components/FileMentionMenu/index.tsx | 2 +- src/ui/components/ModelsDropdown/index.tsx | 2 +- src/ui/components/RawModelDropdown/index.tsx | 2 +- src/ui/components/SkillsDropdown/index.tsx | 4 +- src/ui/components/index.ts | 1 + src/ui/constants.ts | 5 +- src/ui/contexts/RawModeContext.tsx | 2 +- src/ui/index.ts | 2 +- src/ui/utils/index.ts | 170 +++++++++++++++- 14 files changed, 198 insertions(+), 188 deletions(-) rename src/ui/{DropdownMenu.tsx => components/DropdownMenu/index.tsx} (100%) diff --git a/src/common/openai-client.ts b/src/common/openai-client.ts index c1c3e4dd..8c40cdfe 100644 --- a/src/common/openai-client.ts +++ b/src/common/openai-client.ts @@ -3,7 +3,7 @@ import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; import { Agent, fetch as undiciFetch } from "undici"; -import { resolveCurrentSettings } from "../ui/App"; +import { resolveCurrentSettings } from "../ui"; // Custom undici Agent with a 180-second keepAlive timeout. The default // global fetch (undici) only keeps connections alive for 4 seconds, which diff --git a/src/tests/dropdownMenu.test.ts b/src/tests/dropdownMenu.test.ts index 3e4e3ef5..e6a0a1a4 100644 --- a/src/tests/dropdownMenu.test.ts +++ b/src/tests/dropdownMenu.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { calculateVisibleStart } from "../ui/DropdownMenu"; +import { calculateVisibleStart } from "../ui/components/DropdownMenu"; test("calculateVisibleStart centers active item when possible", () => { // 10 items, max 5 visible, active index 4 (middle) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index ae94fa06..24640b03 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,9 +1,6 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; import { createOpenAIClient } from "../common/openai-client"; import { type LlmStreamProgress, @@ -17,17 +14,11 @@ import { type UndoTarget, type UserPromptContent, } from "../session"; -import { - applyModelConfigSelection, - type DeepcodingSettings, - type ModelConfigSelection, - type ResolvedDeepcodingSettings, - resolveSettingsSources, -} from "../settings"; -import { PromptInput, type PromptDraft, type PromptSubmission } from "./PromptInput"; +import { type ModelConfigSelection } from "../settings"; +import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptInput"; import { MessageView, RawModeExitPrompt } from "./components"; import { SessionList } from "./SessionList"; -import { UndoSelector, type UndoRestoreMode } from "./UndoSelector"; +import { type UndoRestoreMode, UndoSelector } from "./UndoSelector"; import { buildLoadingText } from "./loadingText"; import { findExpandedThinkingId } from "./thinkingState"; import { WelcomeScreen } from "./WelcomeScreen"; @@ -43,12 +34,19 @@ import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPromp import { buildExitSummaryText } from "./exitSummary"; import { RawMode, useRawModeContext } from "./contexts"; import { renderMessageToStdout } from "./components/MessageView/utils"; -import { renderRawModeMessages } from "./utils"; +import { + buildPromptDraftFromSessionMessage, + buildStatusLine, + buildSyntheticUserMessage, + formatModelConfig, + isCollapsedThinking, + isCurrentSessionEmpty, + renderRawModeMessages, + resolveCurrentSettings, + writeModelConfigSelection, +} from "./utils"; import { ANSI_CLEAR_SCREEN } from "./constants"; -const DEFAULT_MODEL = "deepseek-v4-pro"; -const DEFAULT_BASE_URL = "https://api.deepseek.com"; - type View = "chat" | "session-list" | "undo" | "mcp-status"; type AppProps = { @@ -807,163 +805,3 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl } export default App; - -function isCollapsedThinking(message: SessionMessage, expandedId: string | null): boolean { - if (message.role !== "assistant") { - return false; - } - if (!message.meta?.asThinking) { - return false; - } - return message.id !== expandedId; -} - -function buildSyntheticUserMessage(content: string, imageCount: number): SessionMessage { - const now = new Date().toISOString(); - return { - id: `local-${Math.random().toString(36).slice(2)}`, - sessionId: "local", - role: "user", - content, - contentParams: - imageCount > 0 - ? Array.from({ length: imageCount }, () => ({ - type: "image_url", - image_url: { url: "" }, - })) - : null, - messageParams: null, - compacted: false, - visible: true, - createTime: now, - updateTime: now, - }; -} - -export function buildPromptDraftFromSessionMessage(message: SessionMessage, nonce: number): PromptDraft { - return { - nonce, - text: typeof message.content === "string" ? message.content : "", - imageUrls: extractImageUrlsFromContentParams(message.contentParams), - }; -} - -function extractImageUrlsFromContentParams(contentParams: unknown): string[] { - const params = Array.isArray(contentParams) ? contentParams : contentParams ? [contentParams] : []; - const imageUrls: string[] = []; - for (const param of params) { - if (!param || typeof param !== "object") { - continue; - } - const record = param as { type?: unknown; image_url?: { url?: unknown } }; - const url = record.image_url?.url; - if (record.type === "image_url" && typeof url === "string" && url) { - imageUrls.push(url); - } - } - return imageUrls; -} - -function isCurrentSessionEmpty(sessionManager: SessionManager): boolean { - const activeSessionId = sessionManager.getActiveSessionId(); - return !activeSessionId || !sessionManager.getSession(activeSessionId); -} - -function buildStatusLine(entry: SessionEntry): string { - const parts: string[] = []; - parts.push(`status: ${entry.status}`); - if (typeof entry.activeTokens === "number" && entry.activeTokens > 0) { - parts.push(`tokens: ${entry.activeTokens}`); - } - if (entry.failReason) { - parts.push(`fail: ${entry.failReason}`); - } - return parts.join(" · "); -} - -export function readSettings(): DeepcodingSettings | null { - return readSettingsFile(getUserSettingsPath()); -} - -export function readProjectSettings(projectRoot: string = process.cwd()): DeepcodingSettings | null { - return readSettingsFile(getProjectSettingsPath(projectRoot)); -} - -function readSettingsFile(settingsPath: string): DeepcodingSettings | null { - try { - if (!fs.existsSync(settingsPath)) { - return null; - } - const raw = fs.readFileSync(settingsPath, "utf8"); - return JSON.parse(raw) as DeepcodingSettings; - } catch { - return null; - } -} - -export function writeSettings(settings: DeepcodingSettings): void { - const settingsPath = getUserSettingsPath(); - writeSettingsFile(settingsPath, settings); -} - -export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { - const settingsPath = getProjectSettingsPath(projectRoot); - writeSettingsFile(settingsPath, settings); -} - -function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { - fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); - fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); -} - -export function writeModelConfigSelection( - selection: ModelConfigSelection, - current: ModelConfigSelection = resolveCurrentSettings(), - projectRoot: string = process.cwd() -): { changed: boolean; settings: DeepcodingSettings } { - const projectSettingsPath = getProjectSettingsPath(projectRoot); - const shouldWriteProjectSettings = fs.existsSync(projectSettingsPath); - const rawSettings = shouldWriteProjectSettings ? readProjectSettings(projectRoot) : readSettings(); - const result = applyModelConfigSelection(rawSettings, current, selection); - if (result.changed) { - if (shouldWriteProjectSettings) { - writeProjectSettings(result.settings, projectRoot); - } else { - writeSettings(result.settings); - } - } - return result; -} - -export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { - return resolveSettingsSources( - readSettings(), - readProjectSettings(projectRoot), - { - model: DEFAULT_MODEL, - baseURL: DEFAULT_BASE_URL, - }, - process.env - ); -} - -export { createOpenAIClient } from "../common/openai-client"; - -function getUserSettingsPath(): string { - return path.join(os.homedir(), ".deepcode", "settings.json"); -} - -function getProjectSettingsPath(projectRoot: string): string { - return path.join(projectRoot, ".deepcode", "settings.json"); -} - -function formatThinkingMode(settings: Pick): string { - if (!settings.thinkingEnabled) { - return "no thinking"; - } - return `thinking ${settings.reasoningEffort}`; -} - -function formatModelConfig(settings: ModelConfigSelection): string { - return `${settings.model}, ${formatThinkingMode(settings)}`; -} diff --git a/src/ui/AppContainer.tsx b/src/ui/AppContainer.tsx index c8b31773..f36eb4aa 100644 --- a/src/ui/AppContainer.tsx +++ b/src/ui/AppContainer.tsx @@ -1,7 +1,7 @@ import React from "react"; import { AppContext } from "./contexts"; import App from "./App"; -import { RawModeProvider } from "./contexts/RawModeContext"; +import { RawModeProvider } from "./contexts"; const AppContainer: React.FC<{ projectRoot: string; diff --git a/src/ui/DropdownMenu.tsx b/src/ui/components/DropdownMenu/index.tsx similarity index 100% rename from src/ui/DropdownMenu.tsx rename to src/ui/components/DropdownMenu/index.tsx diff --git a/src/ui/components/FileMentionMenu/index.tsx b/src/ui/components/FileMentionMenu/index.tsx index ce9a8ee8..b1c77b4a 100644 --- a/src/ui/components/FileMentionMenu/index.tsx +++ b/src/ui/components/FileMentionMenu/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { Box, Text } from "ink"; import { useInput } from "ink"; -import DropdownMenu from "../../DropdownMenu"; +import DropdownMenu from "../DropdownMenu"; import type { FileMentionItem, FileMentionToken } from "../../fileMentions"; type Props = { diff --git a/src/ui/components/ModelsDropdown/index.tsx b/src/ui/components/ModelsDropdown/index.tsx index bdd68ab4..6e807569 100644 --- a/src/ui/components/ModelsDropdown/index.tsx +++ b/src/ui/components/ModelsDropdown/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { useInput } from "ink"; -import DropdownMenu from "../../DropdownMenu"; +import DropdownMenu from "../DropdownMenu"; import type { ModelConfigSelection, ReasoningEffort } from "../../../settings"; type ModelStep = "model" | "thinking"; diff --git a/src/ui/components/RawModelDropdown/index.tsx b/src/ui/components/RawModelDropdown/index.tsx index 33970136..67f053c9 100644 --- a/src/ui/components/RawModelDropdown/index.tsx +++ b/src/ui/components/RawModelDropdown/index.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useInput } from "ink"; -import DropdownMenu from "../../DropdownMenu"; +import DropdownMenu from "../DropdownMenu"; import type { RawMode } from "../../contexts"; import { RAW_COMMAND_MODELS, useRawModeContext } from "../../contexts"; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index b320d249..9704f32b 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,4 +1,4 @@ -import DropdownMenu from "../../DropdownMenu"; +import Index from "../DropdownMenu"; import React, { useEffect, useState } from "react"; import type { SkillInfo } from "../../../session"; import { useInput } from "ink"; @@ -52,7 +52,7 @@ const SkillsDropdown: React.FC<{ } return ( - 0 + ? Array.from({ length: imageCount }, () => ({ + type: "image_url", + image_url: { url: "" }, + })) + : null, + messageParams: null, + compacted: false, + visible: true, + createTime: now, + updateTime: now, + }; +} + +export function buildPromptDraftFromSessionMessage(message: SessionMessage, nonce: number): PromptDraft { + return { + nonce, + text: typeof message.content === "string" ? message.content : "", + imageUrls: extractImageUrlsFromContentParams(message.contentParams), + }; +} + +export function extractImageUrlsFromContentParams(contentParams: unknown): string[] { + const params = Array.isArray(contentParams) ? contentParams : contentParams ? [contentParams] : []; + const imageUrls: string[] = []; + for (const param of params) { + if (!param || typeof param !== "object") { + continue; + } + const record = param as { type?: unknown; image_url?: { url?: unknown } }; + const url = record.image_url?.url; + if (record.type === "image_url" && typeof url === "string" && url) { + imageUrls.push(url); + } + } + return imageUrls; +} + +export function isCurrentSessionEmpty(sessionManager: SessionManager): boolean { + const activeSessionId = sessionManager.getActiveSessionId(); + return !activeSessionId || !sessionManager.getSession(activeSessionId); +} + +export function buildStatusLine(entry: SessionEntry): string { + const parts: string[] = []; + parts.push(`status: ${entry.status}`); + if (typeof entry.activeTokens === "number" && entry.activeTokens > 0) { + parts.push(`tokens: ${entry.activeTokens}`); + } + if (entry.failReason) { + parts.push(`fail: ${entry.failReason}`); + } + return parts.join(" · "); +} + +export function readSettings(): DeepcodingSettings | null { + return readSettingsFile(getUserSettingsPath()); +} + +export function readProjectSettings(projectRoot: string = process.cwd()): DeepcodingSettings | null { + return readSettingsFile(getProjectSettingsPath(projectRoot)); +} + +export function readSettingsFile(settingsPath: string): DeepcodingSettings | null { + try { + if (!fs.existsSync(settingsPath)) { + return null; + } + const raw = fs.readFileSync(settingsPath, "utf8"); + return JSON.parse(raw) as DeepcodingSettings; + } catch { + return null; + } +} + +export function writeSettings(settings: DeepcodingSettings): void { + const settingsPath = getUserSettingsPath(); + writeSettingsFile(settingsPath, settings); +} + +export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { + const settingsPath = getProjectSettingsPath(projectRoot); + writeSettingsFile(settingsPath, settings); +} + +function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); +} + +export function writeModelConfigSelection( + selection: ModelConfigSelection, + current: ModelConfigSelection = resolveCurrentSettings(), + projectRoot: string = process.cwd() +): { changed: boolean; settings: DeepcodingSettings } { + const projectSettingsPath = getProjectSettingsPath(projectRoot); + const shouldWriteProjectSettings = fs.existsSync(projectSettingsPath); + const rawSettings = shouldWriteProjectSettings ? readProjectSettings(projectRoot) : readSettings(); + const result = applyModelConfigSelection(rawSettings, current, selection); + if (result.changed) { + if (shouldWriteProjectSettings) { + writeProjectSettings(result.settings, projectRoot); + } else { + writeSettings(result.settings); + } + } + return result; +} + +export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { + return resolveSettingsSources( + readSettings(), + readProjectSettings(projectRoot), + { + model: DEFAULT_MODEL, + baseURL: DEFAULT_BASE_URL, + }, + process.env + ); +} + +export function getUserSettingsPath(): string { + return path.join(os.homedir(), ".deepcode", "settings.json"); +} + +export function getProjectSettingsPath(projectRoot: string): string { + return path.join(projectRoot, ".deepcode", "settings.json"); +} + +export function formatThinkingMode( + settings: Pick +): string { + if (!settings.thinkingEnabled) { + return "no thinking"; + } + return `thinking ${settings.reasoningEffort}`; +} + +export function formatModelConfig(settings: ModelConfigSelection): string { + return `${settings.model}, ${formatThinkingMode(settings)}`; +} From 09ae2b43f02a5dbb0c1e36e0825d8910b17061b0 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 25 May 2026 16:29:49 +0800 Subject: [PATCH 075/212] chore: clean up non-project files --- Screenshot_2026-05-23_195028.png | 0 docs/issue_0522.md | 241 ------------------------------- 2 files changed, 241 deletions(-) delete mode 100644 Screenshot_2026-05-23_195028.png delete mode 100644 docs/issue_0522.md diff --git a/Screenshot_2026-05-23_195028.png b/Screenshot_2026-05-23_195028.png deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/issue_0522.md b/docs/issue_0522.md deleted file mode 100644 index 2e9fd1a1..00000000 --- a/docs/issue_0522.md +++ /dev/null @@ -1,241 +0,0 @@ -# Deep Code Permission System (设计文档) - -scopes是枚举值,列表如下: - -``` -# PermissionScope -read-in-cwd -read-out-cwd -write-in-cwd -write-out-cwd -delete-in-cwd -delete-out-cwd -query-git-log -mutate-git-log -network -mcp -``` - -settings.json的配置项(例子): - -``` -{ - "permissions": { - "allow": [ - "write-in-cwd" - ], - "deny": [ - "write-out-cwd" - ], - "ask": [ - "read-out-cwd" - ], - "defaultMode": "allowAll|askAll" // 默认是allowAll - } -} -``` - -工具和PermissionScope可能的对应关系: - -- read: read-in-cwd, read-out-cwd -- write: write-in-cwd, write-out-cwd -- edit: write-in-cwd, write-out-cwd -- WebSearch: network -- mcp__*: mcp -- bash: 每一次bash命令需要的scope在sideEffects字段中。如果sideEffects字段为undefined|null,或者sideEffects包含了"unknown"则总是ask -- 其他: 无权限要求,总是允许 - -## bash tool的参数schema新增sideEffects字段 - -目标:让LLM在每一次调用`bash`时显式声明该命令可能需要的权限范围,后端只信任这个结构化字段,不从自然语言`description`中推断权限。 - -需要同步修改两处schema: - -1. `src/prompt.ts`里的`getTools()`内置`bash`工具定义。 -2. `templates/tools/bash.md`里的`bash`工具说明和JSON schema示例。 - -新增字段: - -``` -sideEffects: PermissionScope[] | ["unknown"] -``` - -`bash`可声明的scope只包含文件系统、Git历史和网络权限,不包含`mcp`: - -``` -read-in-cwd -read-out-cwd -write-in-cwd -write-out-cwd -delete-in-cwd -delete-out-cwd -query-git-log -mutate-git-log -network -unknown -``` - -建议schema如下: - -```json -{ - "type": "object", - "properties": { - "command": { - "description": "The command to execute", - "type": "string" - }, - "description": { - "description": "Clear, concise description of what this command does in active voice.", - "type": "string" - }, - "sideEffects": { - "description": "Permission scopes required by this bash command. Use [] only for commands that do not read, write, delete, or access the network. Use [\"unknown\"] when the effects cannot be classified safely.", - "type": "array", - "items": { - "type": "string", - "enum": [ - "read-in-cwd", - "read-out-cwd", - "write-in-cwd", - "write-out-cwd", - "delete-in-cwd", - "delete-out-cwd", - "query-git-log", - "mutate-git-log", - "network", - "unknown" - ] - }, - "uniqueItems": true - } - }, - "required": [ - "command", - "sideEffects" - ], - "additionalProperties": false -} -``` - -字段语义: - -- `sideEffects: []`表示命令不需要权限,例如`date`、`node --version`这类只读取进程环境或输出版本信息的命令。 -- `sideEffects`必须按最小必要权限填写;例如`rg foo src`是`["read-in-cwd"]`,`npm install`通常是`["write-in-cwd", "network"]`。 -- 如果命令访问项目目录之外的路径,需要使用`*-out-cwd`;例如`cat /etc/hosts`是`["read-out-cwd"]`。 -- 删除类操作使用`delete-*`;如果同一条命令还会写入其他文件,再同时声明对应的`write-*`。 -- 查询Git历史使用`query-git-log`;例如`git log`、`git show HEAD`、`git blame`、`git diff HEAD~1..HEAD`这类读取提交历史、提交对象或历史diff的命令。 -- 修改Git历史或引用使用`mutate-git-log`;例如`git commit`、`git reset`、`git rebase`、`git merge`、`git cherry-pick`、`git tag`这类会创建提交、移动引用或改写提交图的命令。 -- Git命令如果同时读写工作区文件,也需要同时声明文件系统scope;例如`git checkout -- src/foo.ts`需要`["write-in-cwd"]`,`git reset --hard HEAD~1`需要`["write-in-cwd", "mutate-git-log"]`。 -- `unknown`只能单独出现为`["unknown"]`,不能和其他scope混用。 - -示例: - -```json -{ "command": "date", "description": "Show current date", "sideEffects": [] } -{ "command": "rg \"TODO\" src", "description": "Search TODO markers in source files", "sideEffects": ["read-in-cwd"] } -{ "command": "npm install", "description": "Install package dependencies", "sideEffects": ["write-in-cwd", "network"] } -{ "command": "rm -rf dist", "description": "Delete build output directory", "sideEffects": ["delete-in-cwd"] } -{ "command": "curl -s https://example.com", "description": "Fetch example.com response", "sideEffects": ["network"] } -{ "command": "git show --stat HEAD", "description": "Show file statistics for HEAD", "sideEffects": ["query-git-log"] } -{ "command": "git blame src/prompt.ts", "description": "Show line authorship for prompt source", "sideEffects": ["read-in-cwd", "query-git-log"] } -{ "command": "git reset --hard HEAD~1", "description": "Reset branch and worktree to previous commit", "sideEffects": ["write-in-cwd", "mutate-git-log"] } -``` - -## 核心数据结构设计 - -``` -export type UserPromptContent = { - text?: string; - imageUrls?: string[]; - skills?: SkillInfo[]; -+ permissions?: [{toolCallId: "...", permission: "allow|deny"}]; -+ alwaysAllows?: [""]; -}; - -export type SessionEntry = { - id: string; - ... - toolCalls: unknown[] | null; // 例如:[{"id":"...","function":{"name":"bash","arguments":"{\"command\": \"...\", \"description\": \"...\"}"}}] - status: SessionStatus; -+ askPermissions?: [{toolCallId: "...", scopes: [""], name: "...", command: "...", description?: "..."}]; -}; - -export type SessionStatus = "... | "completed" | "interrupted" | "ask_permission"; // 新增 ask_permission 状态 - -export type SessionMessage = { - ... - meta?: MessageMeta; - ... -}; - -export type MessageMeta = { - ... -+ permissions?: [{toolCallId: "...", permission: "allow|deny|ask"}]; -+ userPrompt?: UserPromptContent; //对于role为user的消息,持久化userPrompt可方便后续排查问题 -}; -``` - -## 前端流程 - -如果当前会话状态不是ask_permission,则保持现状。会话状态是ask_permission时: - -对SessionEntry.askPermissions中每一个toolCallId的每一个scope,显示权限弹窗(示例): - -``` - - - - - - Do you want to proceed? - ❯ 1. Yes - 2. Yes, and always allow - 3. No -``` - -注意对于read/write/edit的``,格式可以是"工具名称+相对或绝对文件路径",例如:`read ~/dev/main.c` - -如果在权限弹窗过程中,用户按Esc,则走现有的interrupt流程(会话状态也应该变成"interrupted")。 - -提醒注意一种情况:例如askPermissions里面有好几个item的scopes是`["write-in-cwd"]`,如果用户已经在第一个权限弹窗选了"always allow write in CWD `~/dev/qrcode_test/`",则后面的几个scopes是`["write-in-cwd"]`的item就不用显示权限弹窗了。 - -如果用户完成了所有权限弹窗的选择,则判断: - -1. 如果用户提交的结果中包含deny,则需要用户输入user prompt,按回车手动提交replySession()。 - - 如果用户没有输入user prompt就退出了,或者切换到了其他会话。则重新开始这个会话时,由于会话状态还是ask_permission,则会重新显示权限弹窗,要求用户选择。 -2. 如果用户提交的结果中不包含deny,则以`/continue`作为UserPromptContent.text内容,前端自动提交replySession()。 - - -## 后端流程 - -后端主要是对replySession()和activateSession()进行升级: - -1. 支持传入UserPromptContent.permissions和alwaysAllows -2. 如果UserPromptContent.alwaysAllows非空,将其中的scopes追加写入项目级别的settings.json配置文件(`permissions.allow`字段),避免重复写入已存在的项。 -3. 检查当前会话消息列表末尾是否存在连续的role为assistant的有tool_calls的消息,也就是"待执行消息"。如果没有,则走现有流程。 -4. 对于每一条待执行消息,先检查UserPromptContent.permissions中对应的toolCallId的用户授权是allow还是deny - - 如果是allow,则正常执行这个toolCall - - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限。例如: - ``` - { - "ok": false, - "name": "edit", - "error": "用户已禁用了在项目目录之外修改文件的权限,请不要尝试用任何方式修改目录之外的文件" - } - ``` -5. 如果对于某条待执行消息,在UserPromptContent.permissions没有出现对应的toolCallId的用户授权,则检查它的 SessionMessage.meta.permissions[].permission 是allow还是deny还是ask - - 如果是allow,则正常执行这个toolCall - - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限 - - 如果是ask,则直接返回失败结果,报错信息提示LLM用户未授权执行。例如: - ``` - { - "ok": false, - "name": "edit", - "error": "用户暂未授权执行,如果有必要,可重新尝试执行" - } - ``` - - 如果不存在,则正常执行这个toolCall(兼容老版本会话数据) -6. 当LLM返回了新的待执行消息时,不要立即执行,而是: - 1. 根据配置的permissions和defaultMode,计算出SessionMessage.meta.permissions字段 - 2. 如果存在一个待执行消息的SessionMessage.meta.permissions[].permission是ask,则把SessionEntry.status设置为"ask_permission",并设置好SessionEntry.askPermissions,然后退出activateSession,这样就回到了上面的前端流程。 From d07d225a072ffea03bcac41d5b6ab70bc46575cb Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 25 May 2026 16:42:02 +0800 Subject: [PATCH 076/212] 0.1.25 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12b9abc5..dfa3fbb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.24", + "version": "0.1.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.24", + "version": "0.1.25", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index ef70520d..71c171c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.24", + "version": "0.1.25", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From 74a85e9c10f1405c550fa95b259247dac0704a91 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 16:57:32 +0800 Subject: [PATCH 077/212] =?UTF-8?q?refactor(settings):=20=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E8=AF=BB=E5=86=99?= =?UTF-8?q?=E5=8F=8A=E9=BB=98=E8=AE=A4=E5=B8=B8=E9=87=8F=E8=87=B3settings?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将默认模型和基础URL常量移至settings模块,统一管理 - 实现用户配置和项目配置的读取、写入接口 - 提供获取用户和项目配置路径的工具函数 - 清理ui/utils中的相关冗余代码,简化代码结构 - 统一类型引用,调整session、session-types、settings和common之间的导入路径 - 移除AsciiArt模块内容,清理无用代码 --- Screenshot_2026-05-23_195028.png | 0 docs/issue_0522.md | 241 --------------------- src/cli.tsx | 2 +- src/common/openai-client.ts | 2 +- src/{ => common}/updateCheck.ts | 4 +- src/prompt.ts | 5 +- src/session-types.ts | 162 ++++++++++++++ src/session.ts | 190 ++-------------- src/settings.ts | 88 ++++++++ src/tests/askUserQuestion.test.ts | 2 +- src/tests/exitSummary.test.ts | 2 +- src/tests/messageView.test.ts | 2 +- src/tests/promptInputKeys.test.ts | 2 +- src/tests/session.test.ts | 3 +- src/tests/sessionList.test.ts | 2 +- src/tests/slashCommands.test.ts | 2 +- src/tests/thinkingState.test.ts | 2 +- src/tests/updateCheck.test.ts | 2 +- src/ui/App.tsx | 29 ++- src/{ => ui}/AsciiArt.ts | 0 src/ui/AskUserQuestionPrompt.tsx | 2 +- src/ui/PermissionPrompt.tsx | 5 +- src/ui/ProcessStdoutView.tsx | 2 +- src/ui/PromptInput.tsx | 5 +- src/ui/SessionList.tsx | 2 +- src/ui/SlashCommandMenu.tsx | 2 +- src/ui/UndoSelector.tsx | 2 +- src/ui/WelcomeScreen.tsx | 4 +- src/ui/askUserQuestion.ts | 2 +- src/ui/components/MessageView/types.ts | 2 +- src/ui/components/MessageView/utils.ts | 2 +- src/ui/components/SkillsDropdown/index.tsx | 2 +- src/ui/constants.ts | 5 - src/ui/exitSummary.ts | 2 +- src/ui/index.ts | 8 +- src/ui/loadingText.ts | 2 +- src/ui/slashCommands.ts | 2 +- src/ui/thinkingState.ts | 17 +- src/ui/utils/index.ts | 95 +------- 39 files changed, 346 insertions(+), 559 deletions(-) delete mode 100644 Screenshot_2026-05-23_195028.png delete mode 100644 docs/issue_0522.md rename src/{ => common}/updateCheck.ts (98%) create mode 100644 src/session-types.ts rename src/{ => ui}/AsciiArt.ts (100%) diff --git a/Screenshot_2026-05-23_195028.png b/Screenshot_2026-05-23_195028.png deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/issue_0522.md b/docs/issue_0522.md deleted file mode 100644 index 2e9fd1a1..00000000 --- a/docs/issue_0522.md +++ /dev/null @@ -1,241 +0,0 @@ -# Deep Code Permission System (设计文档) - -scopes是枚举值,列表如下: - -``` -# PermissionScope -read-in-cwd -read-out-cwd -write-in-cwd -write-out-cwd -delete-in-cwd -delete-out-cwd -query-git-log -mutate-git-log -network -mcp -``` - -settings.json的配置项(例子): - -``` -{ - "permissions": { - "allow": [ - "write-in-cwd" - ], - "deny": [ - "write-out-cwd" - ], - "ask": [ - "read-out-cwd" - ], - "defaultMode": "allowAll|askAll" // 默认是allowAll - } -} -``` - -工具和PermissionScope可能的对应关系: - -- read: read-in-cwd, read-out-cwd -- write: write-in-cwd, write-out-cwd -- edit: write-in-cwd, write-out-cwd -- WebSearch: network -- mcp__*: mcp -- bash: 每一次bash命令需要的scope在sideEffects字段中。如果sideEffects字段为undefined|null,或者sideEffects包含了"unknown"则总是ask -- 其他: 无权限要求,总是允许 - -## bash tool的参数schema新增sideEffects字段 - -目标:让LLM在每一次调用`bash`时显式声明该命令可能需要的权限范围,后端只信任这个结构化字段,不从自然语言`description`中推断权限。 - -需要同步修改两处schema: - -1. `src/prompt.ts`里的`getTools()`内置`bash`工具定义。 -2. `templates/tools/bash.md`里的`bash`工具说明和JSON schema示例。 - -新增字段: - -``` -sideEffects: PermissionScope[] | ["unknown"] -``` - -`bash`可声明的scope只包含文件系统、Git历史和网络权限,不包含`mcp`: - -``` -read-in-cwd -read-out-cwd -write-in-cwd -write-out-cwd -delete-in-cwd -delete-out-cwd -query-git-log -mutate-git-log -network -unknown -``` - -建议schema如下: - -```json -{ - "type": "object", - "properties": { - "command": { - "description": "The command to execute", - "type": "string" - }, - "description": { - "description": "Clear, concise description of what this command does in active voice.", - "type": "string" - }, - "sideEffects": { - "description": "Permission scopes required by this bash command. Use [] only for commands that do not read, write, delete, or access the network. Use [\"unknown\"] when the effects cannot be classified safely.", - "type": "array", - "items": { - "type": "string", - "enum": [ - "read-in-cwd", - "read-out-cwd", - "write-in-cwd", - "write-out-cwd", - "delete-in-cwd", - "delete-out-cwd", - "query-git-log", - "mutate-git-log", - "network", - "unknown" - ] - }, - "uniqueItems": true - } - }, - "required": [ - "command", - "sideEffects" - ], - "additionalProperties": false -} -``` - -字段语义: - -- `sideEffects: []`表示命令不需要权限,例如`date`、`node --version`这类只读取进程环境或输出版本信息的命令。 -- `sideEffects`必须按最小必要权限填写;例如`rg foo src`是`["read-in-cwd"]`,`npm install`通常是`["write-in-cwd", "network"]`。 -- 如果命令访问项目目录之外的路径,需要使用`*-out-cwd`;例如`cat /etc/hosts`是`["read-out-cwd"]`。 -- 删除类操作使用`delete-*`;如果同一条命令还会写入其他文件,再同时声明对应的`write-*`。 -- 查询Git历史使用`query-git-log`;例如`git log`、`git show HEAD`、`git blame`、`git diff HEAD~1..HEAD`这类读取提交历史、提交对象或历史diff的命令。 -- 修改Git历史或引用使用`mutate-git-log`;例如`git commit`、`git reset`、`git rebase`、`git merge`、`git cherry-pick`、`git tag`这类会创建提交、移动引用或改写提交图的命令。 -- Git命令如果同时读写工作区文件,也需要同时声明文件系统scope;例如`git checkout -- src/foo.ts`需要`["write-in-cwd"]`,`git reset --hard HEAD~1`需要`["write-in-cwd", "mutate-git-log"]`。 -- `unknown`只能单独出现为`["unknown"]`,不能和其他scope混用。 - -示例: - -```json -{ "command": "date", "description": "Show current date", "sideEffects": [] } -{ "command": "rg \"TODO\" src", "description": "Search TODO markers in source files", "sideEffects": ["read-in-cwd"] } -{ "command": "npm install", "description": "Install package dependencies", "sideEffects": ["write-in-cwd", "network"] } -{ "command": "rm -rf dist", "description": "Delete build output directory", "sideEffects": ["delete-in-cwd"] } -{ "command": "curl -s https://example.com", "description": "Fetch example.com response", "sideEffects": ["network"] } -{ "command": "git show --stat HEAD", "description": "Show file statistics for HEAD", "sideEffects": ["query-git-log"] } -{ "command": "git blame src/prompt.ts", "description": "Show line authorship for prompt source", "sideEffects": ["read-in-cwd", "query-git-log"] } -{ "command": "git reset --hard HEAD~1", "description": "Reset branch and worktree to previous commit", "sideEffects": ["write-in-cwd", "mutate-git-log"] } -``` - -## 核心数据结构设计 - -``` -export type UserPromptContent = { - text?: string; - imageUrls?: string[]; - skills?: SkillInfo[]; -+ permissions?: [{toolCallId: "...", permission: "allow|deny"}]; -+ alwaysAllows?: [""]; -}; - -export type SessionEntry = { - id: string; - ... - toolCalls: unknown[] | null; // 例如:[{"id":"...","function":{"name":"bash","arguments":"{\"command\": \"...\", \"description\": \"...\"}"}}] - status: SessionStatus; -+ askPermissions?: [{toolCallId: "...", scopes: [""], name: "...", command: "...", description?: "..."}]; -}; - -export type SessionStatus = "... | "completed" | "interrupted" | "ask_permission"; // 新增 ask_permission 状态 - -export type SessionMessage = { - ... - meta?: MessageMeta; - ... -}; - -export type MessageMeta = { - ... -+ permissions?: [{toolCallId: "...", permission: "allow|deny|ask"}]; -+ userPrompt?: UserPromptContent; //对于role为user的消息,持久化userPrompt可方便后续排查问题 -}; -``` - -## 前端流程 - -如果当前会话状态不是ask_permission,则保持现状。会话状态是ask_permission时: - -对SessionEntry.askPermissions中每一个toolCallId的每一个scope,显示权限弹窗(示例): - -``` - - - - - - Do you want to proceed? - ❯ 1. Yes - 2. Yes, and always allow - 3. No -``` - -注意对于read/write/edit的``,格式可以是"工具名称+相对或绝对文件路径",例如:`read ~/dev/main.c` - -如果在权限弹窗过程中,用户按Esc,则走现有的interrupt流程(会话状态也应该变成"interrupted")。 - -提醒注意一种情况:例如askPermissions里面有好几个item的scopes是`["write-in-cwd"]`,如果用户已经在第一个权限弹窗选了"always allow write in CWD `~/dev/qrcode_test/`",则后面的几个scopes是`["write-in-cwd"]`的item就不用显示权限弹窗了。 - -如果用户完成了所有权限弹窗的选择,则判断: - -1. 如果用户提交的结果中包含deny,则需要用户输入user prompt,按回车手动提交replySession()。 - - 如果用户没有输入user prompt就退出了,或者切换到了其他会话。则重新开始这个会话时,由于会话状态还是ask_permission,则会重新显示权限弹窗,要求用户选择。 -2. 如果用户提交的结果中不包含deny,则以`/continue`作为UserPromptContent.text内容,前端自动提交replySession()。 - - -## 后端流程 - -后端主要是对replySession()和activateSession()进行升级: - -1. 支持传入UserPromptContent.permissions和alwaysAllows -2. 如果UserPromptContent.alwaysAllows非空,将其中的scopes追加写入项目级别的settings.json配置文件(`permissions.allow`字段),避免重复写入已存在的项。 -3. 检查当前会话消息列表末尾是否存在连续的role为assistant的有tool_calls的消息,也就是"待执行消息"。如果没有,则走现有流程。 -4. 对于每一条待执行消息,先检查UserPromptContent.permissions中对应的toolCallId的用户授权是allow还是deny - - 如果是allow,则正常执行这个toolCall - - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限。例如: - ``` - { - "ok": false, - "name": "edit", - "error": "用户已禁用了在项目目录之外修改文件的权限,请不要尝试用任何方式修改目录之外的文件" - } - ``` -5. 如果对于某条待执行消息,在UserPromptContent.permissions没有出现对应的toolCallId的用户授权,则检查它的 SessionMessage.meta.permissions[].permission 是allow还是deny还是ask - - 如果是allow,则正常执行这个toolCall - - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限 - - 如果是ask,则直接返回失败结果,报错信息提示LLM用户未授权执行。例如: - ``` - { - "ok": false, - "name": "edit", - "error": "用户暂未授权执行,如果有必要,可重新尝试执行" - } - ``` - - 如果不存在,则正常执行这个toolCall(兼容老版本会话数据) -6. 当LLM返回了新的待执行消息时,不要立即执行,而是: - 1. 根据配置的permissions和defaultMode,计算出SessionMessage.meta.permissions字段 - 2. 如果存在一个待执行消息的SessionMessage.meta.permissions[].permission是ask,则把SessionEntry.status设置为"ask_permission",并设置好SessionEntry.askPermissions,然后退出activateSession,这样就回到了上面的前端流程。 diff --git a/src/cli.tsx b/src/cli.tsx index c3876ae5..d179203e 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "ink"; import { setShellIfWindows } from "./common/shell-utils"; -import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./updateCheck"; +import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/updateCheck"; import { AppContainer } from "./ui"; const args = process.argv.slice(2); diff --git a/src/common/openai-client.ts b/src/common/openai-client.ts index 8c40cdfe..ee3dd667 100644 --- a/src/common/openai-client.ts +++ b/src/common/openai-client.ts @@ -3,7 +3,7 @@ import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; import { Agent, fetch as undiciFetch } from "undici"; -import { resolveCurrentSettings } from "../ui"; +import { resolveCurrentSettings } from "../settings"; // Custom undici Agent with a 180-second keepAlive timeout. The default // global fetch (undici) only keeps connections alive for 4 seconds, which diff --git a/src/updateCheck.ts b/src/common/updateCheck.ts similarity index 98% rename from src/updateCheck.ts rename to src/common/updateCheck.ts index fcd9bfba..09c0273c 100644 --- a/src/updateCheck.ts +++ b/src/common/updateCheck.ts @@ -5,8 +5,8 @@ import * as os from "os"; import * as path from "path"; import { render, type Instance } from "ink"; import chalk from "chalk"; -import { UpdatePrompt, type UpdatePromptChoice } from "./ui"; -import { killProcessTree } from "./common/process-tree"; +import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; +import { killProcessTree } from "./process-tree"; export type PackageInfo = { name: string; diff --git a/src/prompt.ts b/src/prompt.ts index ba9bf231..4fcd06d6 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -4,7 +4,7 @@ import * as os from "os"; import * as path from "path"; import { fileURLToPath } from "url"; import ejs from "ejs"; -import type { SessionMessage } from "./session"; +import type { SessionMessage } from "./session-types"; import { findGitBashPath, resolveShellPath } from "./common/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; @@ -166,8 +166,7 @@ function getCurrentDateAndModelPrompt(model?: string): string { export function getSystemPrompt(_projectRoot: string, options: PromptToolOptions = {}): string { const toolDocs = readToolDocs(getExtensionRoot(), options); - const basePrompt = toolDocs ? `${SYSTEM_PROMPT_BASE}\n\n# Available Tools\n\n${toolDocs}` : SYSTEM_PROMPT_BASE; - return basePrompt; + return toolDocs ? `${SYSTEM_PROMPT_BASE}\n\n# Available Tools\n\n${toolDocs}` : SYSTEM_PROMPT_BASE; } export function getCompactPrompt(sessionMessages: SessionMessage[]): string { diff --git a/src/session-types.ts b/src/session-types.ts new file mode 100644 index 00000000..33119cc3 --- /dev/null +++ b/src/session-types.ts @@ -0,0 +1,162 @@ +import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; +import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; +import type { CreateOpenAIClient } from "./tools/executor"; +import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; + +export type SessionStatus = + | "failed" + | "pending" + | "processing" + | "waiting_for_user" + | "completed" + | "interrupted" + | "ask_permission" + | "permission_denied"; + +export type ModelUsage = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + completion_tokens_details?: Record; + prompt_tokens_details?: Record; + prompt_cache_hit_tokens?: number; + prompt_cache_miss_tokens?: number; + total_reqs?: number; +}; + +export type SessionProcessEntry = { + startTime: string; + command: string; + timeoutMs?: number; + deadlineAt?: string; + timedOut?: boolean; +}; + +export type BashTimeoutAdjustment = { + processId: string; + timeoutMs: number; + deadlineAt: string; + timedOut: boolean; +}; + +export type SessionEntry = { + id: string; + summary: string | null; + assistantReply: string | null; + assistantThinking: string | null; + assistantRefusal: string | null; + toolCalls: unknown[] | null; + status: SessionStatus; + failReason: string | null; + usage: ModelUsage | null; + usagePerModel: Record | null; + activeTokens: number; + createTime: string; + updateTime: string; + processes: Map | null; + askPermissions?: AskPermissionRequest[]; +}; + +export type SessionsIndex = { + version: 1; + entries: SessionEntry[]; + originalPath: string; +}; + +export type SessionMessageRole = "system" | "user" | "assistant" | "tool"; + +export type MessageMeta = { + function?: unknown; + paramsMd?: string; + resultMd?: string; + asThinking?: boolean; + isSummary?: boolean; + isModelChange?: boolean; + skill?: SkillInfo; + permissions?: MessageToolPermission[]; + userPrompt?: UserPromptContent; +}; + +export type SessionMessage = { + id: string; + sessionId: string; + role: SessionMessageRole; + content: string | null; + contentParams: unknown | null; + messageParams: unknown | null; + compacted: boolean; + visible: boolean; + createTime: string; + updateTime: string; + meta?: MessageMeta; + html?: string; + checkpointHash?: string; +}; + +export type UndoTarget = { + message: SessionMessage; + index: number; + canRestoreCode: boolean; +}; + +export type UserPromptContent = { + text?: string; + imageUrls?: string[]; + skills?: SkillInfo[]; + permissions?: UserToolPermission[]; + alwaysAllows?: PermissionScope[]; +}; + +export type SkillInfo = { + name: string; + path: string; + description: string; + isLoaded?: boolean; +}; + +export type SessionManagerOptions = { + projectRoot: string; + createOpenAIClient: CreateOpenAIClient; + getResolvedSettings: () => { + model: string; + webSearchTool?: string; + mcpServers?: Record; + permissions?: Required; + }; + renderMarkdown: (text: string) => string; + onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; + onSessionEntryUpdated?: (entry: SessionEntry) => void; + onLlmStreamProgress?: (progress: LlmStreamProgress) => void; + onMcpStatusChanged?: () => void; + onProcessStdout?: (pid: number, chunk: string) => void; +}; + +export type LlmStreamProgress = { + requestId: string; + sessionId?: string; + startedAt: string; + estimatedTokens: number; + formattedTokens: string; + phase: "start" | "update" | "end"; +}; + +const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; +const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; + +export function getCompactPromptTokenThreshold(model: string): number { + return DEEPSEEK_V4_MODELS.has(model) + ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD + : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; +} + +function isUsageRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function getTotalTokens(usage: ModelUsage | null | undefined): number { + if (!isUsageRecord(usage)) { + return 0; + } + const totalTokens = (usage as Record).total_tokens; + return typeof totalTokens === "number" ? totalTokens : 0; +} diff --git a/src/session.ts b/src/session.ts index a9fc39e8..bf501671 100644 --- a/src/session.ts +++ b/src/session.ts @@ -5,10 +5,10 @@ import * as crypto from "crypto"; import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; -import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "openai/resources/chat/completions"; +import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; import { launchNotifyScript } from "./common/notify"; import { buildThinkingRequestOptions } from "./common/openai-thinking"; -import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilities"; +import { supportsMultimodal } from "./common/model-capabilities"; import { getCompactPrompt, getDefaultSkillPrompt, @@ -18,15 +18,15 @@ import { type ToolDefinition, } from "./prompt"; import { - ToolExecutor, type CreateOpenAIClient, type ProcessTimeoutControl, type ProcessTimeoutInfo, type ToolCallExecution, type ToolExecutionHooks, + ToolExecutor, } from "./tools/executor"; import { McpManager } from "./mcp/mcp-manager"; -import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; +import type { McpServerConfig, PermissionSettings } from "./settings"; import { logApiError } from "./common/error-logger"; import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; import { killProcessTree } from "./common/process-tree"; @@ -37,29 +37,34 @@ import { buildPermissionToolExecution, computeToolCallPermissions, hasUserPermissionReplies, + type MessageToolPermission, normalizeAskPermissions, parseToolCallForPermissions, - type AskPermissionRequest, - type MessageToolPermission, type PermissionToolCall, type UserToolPermission, } from "./common/permissions"; -export type { PermissionScope } from "./settings"; -export type { - AskPermissionRequest, - AskPermissionScope, - BashPermissionScope, - MessageToolPermission, - PermissionDecision, - UserToolPermission, -} from "./common/permissions"; +import { + type BashTimeoutAdjustment, + getCompactPromptTokenThreshold, + getTotalTokens, + type LlmStreamProgress, + type MessageMeta, + type ModelUsage, + type SessionEntry, + type SessionManagerOptions, + type SessionMessage, + type SessionProcessEntry, + type SessionsIndex, + type SessionStatus, + type SkillInfo, + type UndoTarget, + type UserPromptContent, +} from "./session-types"; const MAX_SESSION_ENTRIES = 50; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; -const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; -const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; type ChatCompletionDebugOptions = { enabled?: boolean; @@ -68,12 +73,6 @@ type ChatCompletionDebugOptions = { params?: Record; }; -export function getCompactPromptTokenThreshold(model: string): number { - return DEEPSEEK_V4_MODELS.has(model) - ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD - : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; -} - function isUsageRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -144,151 +143,6 @@ function getExtensionRoot(): string { return path.resolve(path.dirname(currentFilePath), ".."); } -function getTotalTokens(usage: ModelUsage | null | undefined): number { - if (!isUsageRecord(usage)) { - return 0; - } - const totalTokens = usage.total_tokens; - return typeof totalTokens === "number" ? totalTokens : 0; -} - -export type SessionStatus = - | "failed" - | "pending" - | "processing" - | "waiting_for_user" - | "completed" - | "interrupted" - | "ask_permission" - | "permission_denied"; - -export type ModelUsage = { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - completion_tokens_details?: Record; - prompt_tokens_details?: Record; - prompt_cache_hit_tokens?: number; - prompt_cache_miss_tokens?: number; - total_reqs?: number; -}; - -export type SessionProcessEntry = { - startTime: string; - command: string; - timeoutMs?: number; - deadlineAt?: string; - timedOut?: boolean; -}; - -export type BashTimeoutAdjustment = { - processId: string; - timeoutMs: number; - deadlineAt: string; - timedOut: boolean; -}; - -export type SessionEntry = { - id: string; - summary: string | null; - assistantReply: string | null; - assistantThinking: string | null; - assistantRefusal: string | null; - toolCalls: unknown[] | null; - status: SessionStatus; - failReason: string | null; - usage: ModelUsage | null; - usagePerModel: Record | null; - activeTokens: number; - createTime: string; - updateTime: string; - processes: Map | null; // {pid: process info} - askPermissions?: AskPermissionRequest[]; -}; - -export type SessionsIndex = { - version: 1; - entries: SessionEntry[]; - originalPath: string; -}; - -export type SessionMessageRole = "system" | "user" | "assistant" | "tool"; - -export type MessageMeta = { - function?: unknown; - paramsMd?: string; - resultMd?: string; - asThinking?: boolean; - isSummary?: boolean; - isModelChange?: boolean; - skill?: SkillInfo; - permissions?: MessageToolPermission[]; - userPrompt?: UserPromptContent; -}; - -export type SessionMessage = { - id: string; - sessionId: string; - role: SessionMessageRole; - content: string | null; - contentParams: unknown | null; - messageParams: unknown | null; - compacted: boolean; - visible: boolean; - createTime: string; - updateTime: string; - meta?: MessageMeta; - html?: string; - checkpointHash?: string; -}; - -export type UndoTarget = { - message: SessionMessage; - index: number; - canRestoreCode: boolean; -}; - -export type UserPromptContent = { - text?: string; - imageUrls?: string[]; - skills?: SkillInfo[]; - permissions?: UserToolPermission[]; - alwaysAllows?: PermissionScope[]; -}; - -export type SkillInfo = { - name: string; - path: string; - description: string; - isLoaded?: boolean; -}; - -type SessionManagerOptions = { - projectRoot: string; - createOpenAIClient: CreateOpenAIClient; - getResolvedSettings: () => { - model: string; - webSearchTool?: string; - mcpServers?: Record; - permissions?: Required; - }; - renderMarkdown: (text: string) => string; - onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; - onSessionEntryUpdated?: (entry: SessionEntry) => void; - onLlmStreamProgress?: (progress: LlmStreamProgress) => void; - onMcpStatusChanged?: () => void; - onProcessStdout?: (pid: number, chunk: string) => void; -}; - -export type LlmStreamProgress = { - requestId: string; - sessionId?: string; - startedAt: string; - estimatedTokens: number; - formattedTokens: string; - phase: "start" | "update" | "end"; -}; - export class SessionManager { private readonly projectRoot: string; private readonly createOpenAIClient: CreateOpenAIClient; diff --git a/src/settings.ts b/src/settings.ts index e0b17768..b7a7a777 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,7 @@ import { defaultsToThinkingMode } from "./common/model-capabilities"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; export type DeepcodingEnv = Record & { MODEL?: string; @@ -370,3 +373,88 @@ export function applyModelConfigSelection( return { settings: next, changed: true }; } + +// --------------------------------------------------------------------------- +// Default constants +// --------------------------------------------------------------------------- + +export const DEFAULT_MODEL = "deepseek-v4-pro"; +export const DEFAULT_BASE_URL = "https://api.deepseek.com"; + +// --------------------------------------------------------------------------- +// Settings file I/O +// --------------------------------------------------------------------------- + +export function getUserSettingsPath(): string { + return path.join(os.homedir(), ".deepcode", "settings.json"); +} + +export function getProjectSettingsPath(projectRoot: string): string { + return path.join(projectRoot, ".deepcode", "settings.json"); +} + +export function readSettingsFile(settingsPath: string): DeepcodingSettings | null { + try { + if (!fs.existsSync(settingsPath)) { + return null; + } + const raw = fs.readFileSync(settingsPath, "utf8"); + return JSON.parse(raw) as DeepcodingSettings; + } catch { + return null; + } +} + +export function readSettings(): DeepcodingSettings | null { + return readSettingsFile(getUserSettingsPath()); +} + +export function readProjectSettings(projectRoot: string = process.cwd()): DeepcodingSettings | null { + return readSettingsFile(getProjectSettingsPath(projectRoot)); +} + +function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); +} + +export function writeSettings(settings: DeepcodingSettings): void { + const settingsPath = getUserSettingsPath(); + writeSettingsFile(settingsPath, settings); +} + +export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { + const settingsPath = getProjectSettingsPath(projectRoot); + writeSettingsFile(settingsPath, settings); +} + +export function writeModelConfigSelection( + selection: ModelConfigSelection, + current: ModelConfigSelection = resolveCurrentSettings(), + projectRoot: string = process.cwd() +): { changed: boolean; settings: DeepcodingSettings } { + const projectSettingsPath = getProjectSettingsPath(projectRoot); + const shouldWriteProjectSettings = fs.existsSync(projectSettingsPath); + const rawSettings = shouldWriteProjectSettings ? readProjectSettings(projectRoot) : readSettings(); + const result = applyModelConfigSelection(rawSettings, current, selection); + if (result.changed) { + if (shouldWriteProjectSettings) { + writeProjectSettings(result.settings, projectRoot); + } else { + writeSettings(result.settings); + } + } + return result; +} + +export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { + return resolveSettingsSources( + readSettings(), + readProjectSettings(projectRoot), + { + model: DEFAULT_MODEL, + baseURL: DEFAULT_BASE_URL, + }, + process.env + ); +} diff --git a/src/tests/askUserQuestion.test.ts b/src/tests/askUserQuestion.test.ts index f7543512..89907b07 100644 --- a/src/tests/askUserQuestion.test.ts +++ b/src/tests/askUserQuestion.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, formatAskUserQuestionDecline } from "../ui"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "../session-types"; function message(content: unknown): SessionMessage { const now = "2026-04-29T00:00:00.000Z"; diff --git a/src/tests/exitSummary.test.ts b/src/tests/exitSummary.test.ts index 5ea4b579..651f8b96 100644 --- a/src/tests/exitSummary.test.ts +++ b/src/tests/exitSummary.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildExitSummaryText } from "../ui"; -import type { ModelUsage, SessionEntry } from "../session"; +import type { ModelUsage, SessionEntry } from "../session-types"; const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, ""); diff --git a/src/tests/messageView.test.ts b/src/tests/messageView.test.ts index b806dbd1..c9dfa8b3 100644 --- a/src/tests/messageView.test.ts +++ b/src/tests/messageView.test.ts @@ -8,7 +8,7 @@ import { parseToolPayload, } from "../ui/components/MessageView/utils"; import { RawMode } from "../ui/contexts"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "../session-types"; import type { ToolSummary } from "../ui/components/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts index 4f8b4d95..3b24b213 100644 --- a/src/tests/promptInputKeys.test.ts +++ b/src/tests/promptInputKeys.test.ts @@ -27,7 +27,7 @@ import { insertText, backspace, } from "../ui"; -import type { SessionMessage, SkillInfo } from "../session"; +import type { SessionMessage, SkillInfo } from "../session-types"; function collectDispatchedInput(data: string) { const events: ReturnType[] = []; diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 95de8e35..5615ff55 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -5,7 +5,8 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/file-history"; -import { SessionManager, type SessionMessage } from "../session"; +import { type SessionMessage } from "../session-types"; +import { SessionManager } from "../session"; const originalFetch = globalThis.fetch; const originalConsoleWarn = console.warn; diff --git a/src/tests/sessionList.test.ts b/src/tests/sessionList.test.ts index 6fe41c70..edae36b2 100644 --- a/src/tests/sessionList.test.ts +++ b/src/tests/sessionList.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { formatSessionTitle, filterSessions, formatSessionStatus } from "../ui"; -import type { SessionEntry } from "../session"; +import type { SessionEntry } from "../session-types"; test("formatSessionTitle replaces newlines with spaces", () => { assert.equal(formatSessionTitle("first line\nsecond line\r\nthird"), "first line second line third"); diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts index 30d77eeb..d352f3a5 100644 --- a/src/tests/slashCommands.test.ts +++ b/src/tests/slashCommands.test.ts @@ -7,7 +7,7 @@ import { formatSlashCommandDescription, formatSlashCommandLabel, } from "../ui"; -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "../session-types"; const skills: SkillInfo[] = [ { name: "skill-writer", path: "~/.agents/skills/skill-writer/SKILL.md", description: "Write a SKILL.md" }, diff --git a/src/tests/thinkingState.test.ts b/src/tests/thinkingState.test.ts index 8f2a0e30..f50ab935 100644 --- a/src/tests/thinkingState.test.ts +++ b/src/tests/thinkingState.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findExpandedThinkingId } from "../ui"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "../session-types"; function buildMessage( id: string, diff --git a/src/tests/updateCheck.test.ts b/src/tests/updateCheck.test.ts index ce77fe5e..23682de2 100644 --- a/src/tests/updateCheck.test.ts +++ b/src/tests/updateCheck.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { compareVersions, parseNpmViewVersion } from "../updateCheck"; +import { compareVersions, parseNpmViewVersion } from "../common/updateCheck"; test("compareVersions orders semantic versions", () => { assert.equal(compareVersions("0.1.4", "0.1.3"), 1); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 24640b03..b140574d 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -2,18 +2,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; import { createOpenAIClient } from "../common/openai-client"; -import { - type LlmStreamProgress, - type MessageMeta, - type PermissionScope, - type SessionEntry, - SessionManager, - type SessionMessage, - type SessionStatus, - type SkillInfo, - type UndoTarget, - type UserPromptContent, -} from "../session"; +import type { PermissionScope } from "../settings"; import { type ModelConfigSelection } from "../settings"; import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptInput"; import { MessageView, RawModeExitPrompt } from "./components"; @@ -39,13 +28,23 @@ import { buildStatusLine, buildSyntheticUserMessage, formatModelConfig, - isCollapsedThinking, isCurrentSessionEmpty, renderRawModeMessages, - resolveCurrentSettings, - writeModelConfigSelection, } from "./utils"; +import { resolveCurrentSettings, writeModelConfigSelection } from "../settings"; +import { isCollapsedThinking } from "./thinkingState"; import { ANSI_CLEAR_SCREEN } from "./constants"; +import type { + LlmStreamProgress, + MessageMeta, + SessionEntry, + SessionMessage, + SessionStatus, + SkillInfo, + UndoTarget, + UserPromptContent, +} from "../session-types"; +import { SessionManager } from "../session"; type View = "chat" | "session-list" | "undo" | "mcp-status"; diff --git a/src/AsciiArt.ts b/src/ui/AsciiArt.ts similarity index 100% rename from src/AsciiArt.ts rename to src/ui/AsciiArt.ts diff --git a/src/ui/AskUserQuestionPrompt.tsx b/src/ui/AskUserQuestionPrompt.tsx index 7c76ae38..c84b6200 100644 --- a/src/ui/AskUserQuestionPrompt.tsx +++ b/src/ui/AskUserQuestionPrompt.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; import type { AskUserQuestionAnswers, AskUserQuestionItem } from "./askUserQuestion"; -import { useTerminalInput } from "./PromptInput"; +import { useTerminalInput } from "./prompt"; type Props = { questions: AskUserQuestionItem[]; diff --git a/src/ui/PermissionPrompt.tsx b/src/ui/PermissionPrompt.tsx index 03881a58..dd2d8ebf 100644 --- a/src/ui/PermissionPrompt.tsx +++ b/src/ui/PermissionPrompt.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import type { AskPermissionRequest, AskPermissionScope, PermissionScope, UserToolPermission } from "../session"; -import { useTerminalInput } from "./PromptInput"; +import { useTerminalInput } from "./prompt"; +import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../common/permissions"; +import type { PermissionScope } from "../settings"; export type PermissionPromptResult = { permissions: UserToolPermission[]; diff --git a/src/ui/ProcessStdoutView.tsx b/src/ui/ProcessStdoutView.tsx index bc76a2f1..b47e0cdd 100644 --- a/src/ui/ProcessStdoutView.tsx +++ b/src/ui/ProcessStdoutView.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../common/bash-timeout"; -import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../session"; +import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../session-types"; import { useTerminalInput } from "./prompt"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 8c808e9e..dd124689 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -46,7 +46,6 @@ import { } from "./fileMentions"; import type { FileMentionItem } from "./fileMentions"; import { readClipboardImageAsync } from "./clipboard"; -import type { PermissionScope, SessionEntry, SkillInfo, UserToolPermission } from "../session"; // Re-exported from prompt modules for backward compatibility export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./prompt"; @@ -61,8 +60,10 @@ import { useTerminalFocusReporting, } from "./prompt"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; -import type { ModelConfigSelection } from "../settings"; +import type { ModelConfigSelection, PermissionScope } from "../settings"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; +import type { SessionEntry, SkillInfo } from "../session-types"; +import type { UserToolPermission } from "../common/permissions"; export type PromptSubmission = { text: string; diff --git a/src/ui/SessionList.tsx b/src/ui/SessionList.tsx index 2d83b847..4ea620e7 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/SessionList.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { SessionEntry, SessionStatus } from "../session"; +import type { SessionEntry, SessionStatus } from "../session-types"; import { truncate } from "./components/MessageView/utils"; type Props = { diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/SlashCommandMenu.tsx index df599b54..ddd79251 100644 --- a/src/ui/SlashCommandMenu.tsx +++ b/src/ui/SlashCommandMenu.tsx @@ -3,7 +3,7 @@ import type { SlashCommandItem } from "./slashCommands"; import { ARGS_SEPARATOR } from "./constants"; import React from "react"; import { Box, Text } from "ink"; -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "../session-types"; type SlashCommandMenuProps = { items: SlashCommandItem[]; diff --git a/src/ui/UndoSelector.tsx b/src/ui/UndoSelector.tsx index fad3e178..e41993e2 100644 --- a/src/ui/UndoSelector.tsx +++ b/src/ui/UndoSelector.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { UndoTarget } from "../session"; +import type { UndoTarget } from "../session-types"; export type UndoRestoreMode = "code-and-conversation" | "conversation"; diff --git a/src/ui/WelcomeScreen.tsx b/src/ui/WelcomeScreen.tsx index 7e740d1f..2e6b7cfa 100644 --- a/src/ui/WelcomeScreen.tsx +++ b/src/ui/WelcomeScreen.tsx @@ -2,11 +2,11 @@ import React, { useMemo, useState } from "react"; import { Box, Text } from "ink"; import * as os from "node:os"; import path from "node:path"; -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "../session-types"; import type { ResolvedDeepcodingSettings } from "../settings"; import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./slashCommands"; import { ThemedGradient } from "./ThemedGradient"; -import { AsciiLogo } from "../AsciiArt"; +import { AsciiLogo } from "./AsciiArt"; import { useAppContext } from "./contexts"; type WelcomeScreenProps = { diff --git a/src/ui/askUserQuestion.ts b/src/ui/askUserQuestion.ts index 8d168d86..813f7cf8 100644 --- a/src/ui/askUserQuestion.ts +++ b/src/ui/askUserQuestion.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../session"; +import type { SessionMessage, SessionStatus } from "../session-types"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/components/MessageView/types.ts b/src/ui/components/MessageView/types.ts index 743eb2dc..3d734f89 100644 --- a/src/ui/components/MessageView/types.ts +++ b/src/ui/components/MessageView/types.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../../session"; +import type { SessionMessage } from "../../../session-types"; export type MessageViewProps = { message: SessionMessage; diff --git a/src/ui/components/MessageView/utils.ts b/src/ui/components/MessageView/utils.ts index af5391d8..164b5fb4 100644 --- a/src/ui/components/MessageView/utils.ts +++ b/src/ui/components/MessageView/utils.ts @@ -1,5 +1,5 @@ import type { DiffPreviewLine, ToolSummary } from "./types"; -import type { SessionMessage } from "../../../session"; +import type { SessionMessage } from "../../../session-types"; import { RawMode } from "../../contexts"; import chalk from "chalk"; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index 9704f32b..db446040 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,6 +1,6 @@ import Index from "../DropdownMenu"; import React, { useEffect, useState } from "react"; -import type { SkillInfo } from "../../../session"; +import type { SkillInfo } from "../../../session-types"; import { useInput } from "ink"; import { isSkillSelected } from "../../SlashCommandMenu"; diff --git a/src/ui/constants.ts b/src/ui/constants.ts index ad5ed0e1..7b336f10 100644 --- a/src/ui/constants.ts +++ b/src/ui/constants.ts @@ -1,8 +1,3 @@ -/** Default model to use for completions. */ -export const DEFAULT_MODEL = "deepseek-v4-pro"; -/** Default base URL for API requests. */ -export const DEFAULT_BASE_URL = "https://api.deepseek.com"; - /** Separator used when rendering command arguments inline (e.g., `arg1 | arg2 | arg3`). */ export const ARGS_SEPARATOR = " | "; diff --git a/src/ui/exitSummary.ts b/src/ui/exitSummary.ts index c55d9ce8..95e11e7b 100644 --- a/src/ui/exitSummary.ts +++ b/src/ui/exitSummary.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import gradientString from "gradient-string"; -import type { ModelUsage, SessionEntry } from "../session"; +import type { ModelUsage, SessionEntry } from "../session-types"; type ExitSummaryInput = { session: SessionEntry | null; diff --git a/src/ui/index.ts b/src/ui/index.ts index 2ac3ee7c..f3cd41a7 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -11,8 +11,10 @@ export { writeProjectSettings, writeModelConfigSelection, resolveCurrentSettings, - buildPromptDraftFromSessionMessage, -} from "./utils"; + DEFAULT_MODEL, + DEFAULT_BASE_URL, +} from "../settings"; +export { buildPromptDraftFromSessionMessage } from "./utils"; export { createOpenAIClient } from "../common/openai-client"; export { default as AppContainer } from "./AppContainer"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; @@ -95,5 +97,5 @@ export { type FileMentionItem, type FileMentionToken, } from "./fileMentions"; -export { findExpandedThinkingId } from "./thinkingState"; +export { findExpandedThinkingId, isCollapsedThinking } from "./thinkingState"; export { buildExitSummaryText } from "./exitSummary"; diff --git a/src/ui/loadingText.ts b/src/ui/loadingText.ts index bfb97d4c..71304055 100644 --- a/src/ui/loadingText.ts +++ b/src/ui/loadingText.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../session"; +import type { LlmStreamProgress, SessionEntry } from "../session-types"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/slashCommands.ts b/src/ui/slashCommands.ts index 6d9b7cc1..2677a231 100644 --- a/src/ui/slashCommands.ts +++ b/src/ui/slashCommands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "../session-types"; export type SlashCommandKind = | "skill" diff --git a/src/ui/thinkingState.ts b/src/ui/thinkingState.ts index 6f419e24..aad6d212 100644 --- a/src/ui/thinkingState.ts +++ b/src/ui/thinkingState.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "../session-types"; /** * Returns the message id of the assistant "thinking" message that should stay @@ -21,3 +21,18 @@ export function findExpandedThinkingId(messages: SessionMessage[]): string | nul } return expanded; } + +/** + * Returns whether a message's thinking block should be rendered collapsed. + * A thinking message is collapsed when its id does not match the currently + * expanded thinking id. + */ +export function isCollapsedThinking(message: SessionMessage, expandedId: string | null): boolean { + if (message.role !== "assistant") { + return false; + } + if (!message.meta?.asThinking) { + return false; + } + return message.id !== expandedId; +} diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index bded46bb..4a201466 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -1,15 +1,10 @@ import chalk from "chalk"; -import type { SessionManager, SessionMessage } from "../../session"; -import { type SessionEntry } from "../../session"; import { renderMessageToStdout } from "../components/MessageView/utils"; import type { RawMode } from "../contexts"; import type { PromptDraft } from "../PromptInput"; -import type { DeepcodingSettings, ModelConfigSelection } from "../../settings"; -import { applyModelConfigSelection, type ResolvedDeepcodingSettings, resolveSettingsSources } from "../../settings"; -import fs from "fs"; -import path from "path"; -import os from "os"; -import { DEFAULT_BASE_URL, DEFAULT_MODEL } from "../constants"; +import type { ModelConfigSelection } from "../../settings"; +import type { SessionEntry, SessionMessage } from "../../session-types"; +import type { SessionManager } from "../../session"; /** * Render all messages directly to stdout for Raw mode display. @@ -31,16 +26,6 @@ export function renderRawModeMessages(allMessages: SessionMessage[], mode: strin } } -export function isCollapsedThinking(message: SessionMessage, expandedId: string | null): boolean { - if (message.role !== "assistant") { - return false; - } - if (!message.meta?.asThinking) { - return false; - } - return message.id !== expandedId; -} - export function buildSyntheticUserMessage(content: string, imageCount: number): SessionMessage { const now = new Date().toISOString(); return { @@ -104,80 +89,6 @@ export function buildStatusLine(entry: SessionEntry): string { return parts.join(" · "); } -export function readSettings(): DeepcodingSettings | null { - return readSettingsFile(getUserSettingsPath()); -} - -export function readProjectSettings(projectRoot: string = process.cwd()): DeepcodingSettings | null { - return readSettingsFile(getProjectSettingsPath(projectRoot)); -} - -export function readSettingsFile(settingsPath: string): DeepcodingSettings | null { - try { - if (!fs.existsSync(settingsPath)) { - return null; - } - const raw = fs.readFileSync(settingsPath, "utf8"); - return JSON.parse(raw) as DeepcodingSettings; - } catch { - return null; - } -} - -export function writeSettings(settings: DeepcodingSettings): void { - const settingsPath = getUserSettingsPath(); - writeSettingsFile(settingsPath, settings); -} - -export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { - const settingsPath = getProjectSettingsPath(projectRoot); - writeSettingsFile(settingsPath, settings); -} - -function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { - fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); - fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); -} - -export function writeModelConfigSelection( - selection: ModelConfigSelection, - current: ModelConfigSelection = resolveCurrentSettings(), - projectRoot: string = process.cwd() -): { changed: boolean; settings: DeepcodingSettings } { - const projectSettingsPath = getProjectSettingsPath(projectRoot); - const shouldWriteProjectSettings = fs.existsSync(projectSettingsPath); - const rawSettings = shouldWriteProjectSettings ? readProjectSettings(projectRoot) : readSettings(); - const result = applyModelConfigSelection(rawSettings, current, selection); - if (result.changed) { - if (shouldWriteProjectSettings) { - writeProjectSettings(result.settings, projectRoot); - } else { - writeSettings(result.settings); - } - } - return result; -} - -export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { - return resolveSettingsSources( - readSettings(), - readProjectSettings(projectRoot), - { - model: DEFAULT_MODEL, - baseURL: DEFAULT_BASE_URL, - }, - process.env - ); -} - -export function getUserSettingsPath(): string { - return path.join(os.homedir(), ".deepcode", "settings.json"); -} - -export function getProjectSettingsPath(projectRoot: string): string { - return path.join(projectRoot, ".deepcode", "settings.json"); -} - export function formatThinkingMode( settings: Pick ): string { From 33fdcd63e41d2296dbe8d389d9d91d51453f6339 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 17:54:15 +0800 Subject: [PATCH 078/212] =?UTF-8?q?refactor(prompt):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=B2=98=E8=B4=B4=E5=92=8C=E5=8E=86=E5=8F=B2=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将粘贴处理逻辑提取到单独的 usePasteHandling hook 中 - 将历史导航逻辑提取到单独的 useHistoryNavigation hook 中 - 用 hook 替代 PromptInput 内部状态管理,简化组件代码 - 支持大文本粘贴显示可折叠的占位符标记 - 实现粘贴内容的展开与折叠控制 - 重置粘贴状态时清理所有历史数据 - 修正状态同步,提升粘贴和历史浏览体验 - 对外提供相关类型定义和状态、操作接口 --- src/ui/PromptInput.tsx | 152 ++++------------------------ src/ui/prompt/history-navigation.ts | 65 ++++++++++++ src/ui/prompt/index.ts | 6 ++ src/ui/prompt/paste-handling.ts | 150 +++++++++++++++++++++++++++ 4 files changed, 240 insertions(+), 133 deletions(-) create mode 100644 src/ui/prompt/history-navigation.ts create mode 100644 src/ui/prompt/paste-handling.ts diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index dd124689..ab8955ef 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -6,16 +6,13 @@ import { EMPTY_BUFFER, PASTE_MARKER_REGEX, backspace, - cleanPasteContent, deleteForward, deletePasteMarkerBackward, deletePasteMarkerForward, deleteWordBefore, deleteWordAfter, expandPasteMarkers, - findPasteMarkerContaining, getCurrentSlashToken, - hasActivePasteMarkers, insertText, isEmpty, killLine, @@ -53,6 +50,8 @@ export type { InputKey } from "./prompt"; import { useTerminalInput } from "./prompt"; import type { InputKey } from "./prompt"; +import { usePasteHandling } from "./prompt/paste-handling"; +import { useHistoryNavigation } from "./prompt/history-navigation"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, @@ -150,21 +149,22 @@ export const PromptInput = React.memo(function PromptInput({ const [showModelDropdown, setShowModelDropdown] = useState(false); const [fileMentionItems, setFileMentionItems] = useState(() => scanFileMentionItems(projectRoot)); const [dismissedFileMentionKey, setDismissedFileMentionKey] = useState(null); - const [historyCursor, setHistoryCursor] = useState(-1); - const [draftBeforeHistory, setDraftBeforeHistory] = useState(null); const [hasTerminalFocus, setHasTerminalFocus] = useState(true); const lastCtrlDAt = React.useRef(0); const undoRedoRef = React.useRef(createPromptUndoRedoState()); const wasBusyRef = React.useRef(busy); const hadFileMentionTokenRef = React.useRef(false); const appliedDraftNonceRef = React.useRef(null); - const pastesRef = React.useRef>(new Map()); - const pasteCounterRef = React.useRef(0); - // Track expanded paste regions for toggle (Ctrl+O expand / collapse). - const expandedRegionsRef = React.useRef>( - new Map() + + const { historyCursor, navigateHistory, exitHistoryBrowsing } = useHistoryNavigation( + buffer, + setBuffer, + promptHistory ); + const { pastesRef, handlePaste, expandPasteMarkerAtCursor, resetPastes, hasCollapsedMarkers, hasExpandedRegions } = + usePasteHandling(buffer, updateBuffer, setStatusMessage); + const fileMentionToken = getCurrentFileMentionToken(buffer); const hasFileMentionToken = fileMentionToken !== null; const fileMentionKey = fileMentionToken ? `${fileMentionToken.start}:${fileMentionToken.query}` : null; @@ -191,8 +191,6 @@ export const PromptInput = React.memo(function PromptInput({ const showMenu = slashMenu.length > 0; const promptHistoryKey = React.useMemo(() => promptHistory.join("\0"), [promptHistory]); const hasRunningProcess = runningProcesses && runningProcesses.size > 0; - const hasCollapsedMarkers = hasActivePasteMarkers(buffer.text, pastesRef.current); - const hasExpandedRegions = expandedRegionsRef.current.size > 0; const processOrPasteHint = hasRunningProcess ? " · ctrl+o view output" : hasCollapsedMarkers @@ -268,17 +266,14 @@ export const PromptInput = React.memo(function PromptInput({ setSelectedSkills([]); setShowSkillsDropdown(false); setOpenRawModelDropdown(false); - setHistoryCursor(-1); - setDraftBeforeHistory(null); + exitHistoryBrowsing(); clearPromptUndoRedoState(undoRedoRef.current); - pastesRef.current.clear(); - expandedRegionsRef.current.clear(); - }, [promptDraft]); + resetPastes(); + }, [promptDraft, exitHistoryBrowsing, resetPastes]); useEffect(() => { - setHistoryCursor(-1); - setDraftBeforeHistory(null); - }, [promptHistoryKey]); + exitHistoryBrowsing(); + }, [promptHistoryKey, exitHistoryBrowsing]); useTerminalInput( (input, key) => { @@ -338,8 +333,7 @@ export const PromptInput = React.memo(function PromptInput({ } else if (!isEmpty(buffer)) { setBuffer(EMPTY_BUFFER); clearUndoRedoStacks(); - pastesRef.current.clear(); - expandedRegionsRef.current.clear(); + resetPastes(); } else { setStatusMessage("press ctrl+d to exit"); } @@ -529,8 +523,7 @@ export const PromptInput = React.memo(function PromptInput({ } if (key.ctrl && (input === "u" || input === "U")) { updateBuffer(() => EMPTY_BUFFER); - pastesRef.current.clear(); - expandedRegionsRef.current.clear(); + resetPastes(); return; } if (key.ctrl && (input === "w" || input === "W")) { @@ -594,11 +587,6 @@ export const PromptInput = React.memo(function PromptInput({ clearPromptUndoRedoState(undoRedoRef.current); } - function exitHistoryBrowsing(): void { - setHistoryCursor(-1); - setDraftBeforeHistory(null); - } - function updateBuffer(updater: (state: PromptBufferState) => PromptBufferState): void { exitHistoryBrowsing(); setBuffer((current) => { @@ -608,107 +596,6 @@ export const PromptInput = React.memo(function PromptInput({ }); } - function handlePaste(pastedText: string): void { - const totalChars = pastedText.length; - - if (totalChars <= 1000) { - const newlineCount = (pastedText.match(/\n/g) ?? []).length; - if (newlineCount <= 9) { - const clean = pastedText - .replace(/\r\n|\r/g, "\n") - .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") - .replace(/\t/g, " "); - updateBuffer((s) => insertText(s, clean)); - return; - } - } - - // Large paste: store raw text, insert marker with line/char count. - const lineCount = (pastedText.match(/\n/g) ?? []).length + 1; - pasteCounterRef.current += 1; - const pasteId = pasteCounterRef.current; - pastesRef.current.set(pasteId, pastedText); - - const marker = - lineCount > 10 ? `[paste #${pasteId} +${lineCount} lines]` : `[paste #${pasteId} ${totalChars} chars]`; - - updateBuffer((s) => insertText(s, marker)); - } - - function expandPasteMarkerAtCursor(): void { - // First, try to collapse an already-expanded region at the cursor. - for (const [id, region] of expandedRegionsRef.current) { - if (buffer.cursor >= region.start && buffer.cursor <= region.end) { - // Collapse back to marker. - expandedRegionsRef.current.delete(id); - pastesRef.current.set(id, region.content); - setTimeout(() => { - updateBuffer((s) => { - const text = s.text.slice(0, region.start) + region.marker + s.text.slice(region.end); - return { text, cursor: region.start + region.marker.length }; - }); - }, 0); - return; - } - } - - // No expanded region at cursor — try to expand a paste marker. - const marker = findPasteMarkerContaining(buffer); - if (!marker) { - setStatusMessage("No paste marker at cursor"); - return; - } - const content = pastesRef.current.get(marker.id); - if (!content) { - setStatusMessage("Paste content not found"); - return; - } - - const pasteId = marker.id; - const originalMarker = buffer.text.slice(marker.start, marker.end); - pastesRef.current.delete(pasteId); - - setTimeout(() => { - updateBuffer((s) => { - const text = s.text.slice(0, marker.start) + cleanPasteContent(content) + s.text.slice(marker.end); - const newEnd = marker.start + content.length; - expandedRegionsRef.current.set(pasteId, { - start: marker.start, - end: newEnd, - content, - marker: originalMarker, - }); - return { text, cursor: marker.start }; - }); - }, 0); - } - - function navigateHistory(direction: -1 | 1): void { - if (promptHistory.length === 0) { - return; - } - - const previousCursor = historyCursor === -1 ? promptHistory.length : historyCursor; - const nextCursor = Math.max(0, Math.min(promptHistory.length, previousCursor + direction)); - const draft = historyCursor === -1 ? buffer.text : draftBeforeHistory; - - if (historyCursor === -1) { - setDraftBeforeHistory(buffer.text); - } - - if (nextCursor === promptHistory.length) { - const text = draft ?? ""; - setBuffer({ text, cursor: text.length }); - setHistoryCursor(-1); - setDraftBeforeHistory(null); - return; - } - - const text = promptHistory[nextCursor] ?? ""; - setBuffer({ text, cursor: text.length }); - setHistoryCursor(nextCursor); - } - function insertFileMentionSelection(item: FileMentionItem): void { if (!fileMentionToken) { return; @@ -723,9 +610,8 @@ export const PromptInput = React.memo(function PromptInput({ setImageUrls([]); setSelectedSkills([]); setShowSkillsDropdown(false); - pastesRef.current.clear(); - expandedRegionsRef.current.clear(); - pasteCounterRef.current = 0; + exitHistoryBrowsing(); + resetPastes(); } function handleSlashSelection(item: SlashCommandItem): void { diff --git a/src/ui/prompt/history-navigation.ts b/src/ui/prompt/history-navigation.ts new file mode 100644 index 00000000..22ad0f63 --- /dev/null +++ b/src/ui/prompt/history-navigation.ts @@ -0,0 +1,65 @@ +import type React from "react"; +import { useCallback, useState } from "react"; +import type { PromptBufferState } from "../promptBuffer"; + +export type HistoryNavigationState = { + historyCursor: number; + draftBeforeHistory: string | null; +}; + +export type HistoryNavigationActions = { + /** + * Navigate through prompt history. Pass -1 for previous, 1 for next. + * Stores current draft before entering history mode and restores it when + * scrolling past the last entry. + */ + navigateHistory: (direction: -1 | 1) => void; + /** Exit history browsing mode, restoring the pre-history draft if any. */ + exitHistoryBrowsing: () => void; +}; + +export function useHistoryNavigation( + buffer: PromptBufferState, + setBuffer: React.Dispatch>, + promptHistory: string[] +): HistoryNavigationState & HistoryNavigationActions { + const [historyCursor, setHistoryCursor] = useState(-1); + const [draftBeforeHistory, setDraftBeforeHistory] = useState(null); + + const exitHistoryBrowsing = useCallback((): void => { + setHistoryCursor(-1); + setDraftBeforeHistory(null); + }, []); + + function navigateHistory(direction: -1 | 1): void { + if (promptHistory.length === 0) { + return; + } + + const previousCursor = historyCursor === -1 ? promptHistory.length : historyCursor; + const nextCursor = Math.max(0, Math.min(promptHistory.length, previousCursor + direction)); + + if (historyCursor === -1) { + setDraftBeforeHistory(buffer.text); + } + + if (nextCursor === promptHistory.length) { + const text = draftBeforeHistory ?? ""; + setBuffer({ text, cursor: text.length }); + setHistoryCursor(-1); + setDraftBeforeHistory(null); + return; + } + + const text = promptHistory[nextCursor] ?? ""; + setBuffer({ text, cursor: text.length }); + setHistoryCursor(nextCursor); + } + + return { + historyCursor, + draftBeforeHistory, + navigateHistory, + exitHistoryBrowsing, + }; +} diff --git a/src/ui/prompt/index.ts b/src/ui/prompt/index.ts index 6435f620..56e07251 100644 --- a/src/ui/prompt/index.ts +++ b/src/ui/prompt/index.ts @@ -9,3 +9,9 @@ export { useTerminalFocusReporting, getPromptCursorPlacement, } from "./cursor"; + +export { usePasteHandling } from "./paste-handling"; +export type { PasteRegion, PasteHandlingState, PasteHandlingActions } from "./paste-handling"; + +export { useHistoryNavigation } from "./history-navigation"; +export type { HistoryNavigationState, HistoryNavigationActions } from "./history-navigation"; diff --git a/src/ui/prompt/paste-handling.ts b/src/ui/prompt/paste-handling.ts new file mode 100644 index 00000000..63c2c547 --- /dev/null +++ b/src/ui/prompt/paste-handling.ts @@ -0,0 +1,150 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import type { PromptBufferState } from "../promptBuffer"; +import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../promptBuffer"; + +export type PasteRegion = { + start: number; + end: number; + content: string; + marker: string; +}; + +export type PasteHandlingState = { + /** Ref holding all paste content keyed by paste ID. */ + pastesRef: React.RefObject>; + /** Ref holding expanded paste regions for Ctrl+O toggle. */ + expandedRegionsRef: React.RefObject>; + /** Counter for generating unique paste IDs. */ + pasteCounterRef: React.RefObject; + /** Whether any paste marker is currently collapsed. */ + hasCollapsedMarkers: boolean; + /** Whether any paste region has been expanded. */ + hasExpandedRegions: boolean; +}; + +export type PasteHandlingActions = { + /** + * Process pasted text. Short pastes (<1000 chars, ≤9 newlines) are inserted + * inline. Larger pastes receive a collapsible marker. + */ + handlePaste: (pastedText: string) => void; + /** Expand a collapsed paste marker at the cursor, or collapse an expanded region. */ + expandPasteMarkerAtCursor: () => void; + /** Reset all paste-related state. */ + resetPastes: () => void; +}; + +export function usePasteHandling( + buffer: PromptBufferState, + updateBuffer: (updater: (state: PromptBufferState) => PromptBufferState) => void, + setStatusMessage: (msg: string | null) => void +): PasteHandlingState & PasteHandlingActions { + const pastesRef = useRef>(new Map()); + const pasteCounterRef = useRef(0); + const expandedRegionsRef = useRef>(new Map()); + const [hasCollapsedMarkers, setHasCollapsedMarkers] = useState(false); + const [hasExpandedRegions, setHasExpandedRegions] = useState(false); + + function refreshDerivedFlags(): void { + setHasCollapsedMarkers(hasActivePasteMarkers(buffer.text, pastesRef.current)); + setHasExpandedRegions(expandedRegionsRef.current.size > 0); + } + + function handlePaste(pastedText: string): void { + const totalChars = pastedText.length; + + if (totalChars <= 1000) { + const newlineCount = (pastedText.match(/\n/g) ?? []).length; + if (newlineCount <= 9) { + const clean = pastedText + .replace(/\r\n|\r/g, "\n") + .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") + .replace(/\t/g, " "); + updateBuffer((s) => insertText(s, clean)); + return; + } + } + + // Large paste: store raw text, insert marker. + const lineCount = (pastedText.match(/\n/g) ?? []).length + 1; + pasteCounterRef.current += 1; + const pasteId = pasteCounterRef.current; + pastesRef.current.set(pasteId, pastedText); + + const marker = + lineCount > 10 ? `[paste #${pasteId} +${lineCount} lines]` : `[paste #${pasteId} ${totalChars} chars]`; + + updateBuffer((s) => insertText(s, marker)); + refreshDerivedFlags(); + } + + function expandPasteMarkerAtCursor(): void { + // Collapse an already-expanded region at the cursor. + for (const [id, region] of expandedRegionsRef.current) { + if (buffer.cursor >= region.start && buffer.cursor <= region.end) { + expandedRegionsRef.current.delete(id); + pastesRef.current.set(id, region.content); + setTimeout(() => { + updateBuffer((s) => { + const text = s.text.slice(0, region.start) + region.marker + s.text.slice(region.end); + return { text, cursor: region.start + region.marker.length }; + }); + refreshDerivedFlags(); + }, 0); + refreshDerivedFlags(); + return; + } + } + + // Expand a paste marker. + const marker = findPasteMarkerContaining(buffer); + if (!marker) { + setStatusMessage("No paste marker at cursor"); + return; + } + const content = pastesRef.current.get(marker.id); + if (!content) { + setStatusMessage("Paste content not found"); + return; + } + + const pasteId = marker.id; + const originalMarker = buffer.text.slice(marker.start, marker.end); + pastesRef.current.delete(pasteId); + + setTimeout(() => { + updateBuffer((s) => { + const text = s.text.slice(0, marker.start) + cleanPasteContent(content) + s.text.slice(marker.end); + const newEnd = marker.start + content.length; + expandedRegionsRef.current.set(pasteId, { + start: marker.start, + end: newEnd, + content, + marker: originalMarker, + }); + return { text, cursor: marker.start }; + }); + refreshDerivedFlags(); + }, 0); + refreshDerivedFlags(); + } + + function resetPastes(): void { + pastesRef.current.clear(); + expandedRegionsRef.current.clear(); + pasteCounterRef.current = 0; + refreshDerivedFlags(); + } + + return { + pastesRef, + expandedRegionsRef, + pasteCounterRef, + hasCollapsedMarkers, + hasExpandedRegions, + handlePaste, + expandPasteMarkerAtCursor, + resetPastes, + }; +} From febcd93593d2a435cd83eafd5857cd8394719ba8 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 18:19:48 +0800 Subject: [PATCH 079/212] =?UTF-8?q?refactor(core):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=EF=BC=8C=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E6=A8=A1=E5=9D=97=E8=87=B3=20core=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 ui 目录下的多个模块移动至 ui/core 目录,调整相关导入路径以适配新结构 - 将 common 目录下部分模块重命名移动至 common/runtime 和 common/system,更新导入引用 - 更新测试文件中相关导入路径,确保测试代码正常执行 - 新增 session/utils.ts,抽取并集中管理 getCompactPromptTokenThreshold 与 getTotalTokens 方法 - 调整部分导入钩子名称由 prompt 变更为 hooks,提升代码组织一致性 - 修正部分日志及进程管理模块的导入路径到对应的 logging 和 system 子目录 - 更新 UI 组件和工具相关代码以适应新的模块路径和结构变化 --- src/cli.tsx | 2 +- src/common/file-utils.ts | 2 +- src/common/{ => logging}/debug-logger.ts | 0 src/common/{ => logging}/error-logger.ts | 0 src/common/permissions.ts | 2 +- src/common/{ => runtime}/file-history.ts | 0 src/common/{ => runtime}/runtime.ts | 2 +- src/common/{ => runtime}/state.ts | 2 +- src/common/{ => system}/bash-timeout.ts | 0 src/common/{ => system}/process-tree.ts | 0 src/common/{ => system}/shell-utils.ts | 0 src/common/updateCheck.ts | 2 +- src/mcp/mcp-client.ts | 2 +- src/prompt.ts | 2 +- src/session-types.ts | 22 -------------- src/session.ts | 13 ++++---- src/session/utils.ts | 23 ++++++++++++++ src/tests/clipboard.test.ts | 6 ++-- src/tests/debug-logger.test.ts | 2 +- src/tests/fileMentions.test.ts | 2 +- src/tests/process-tree.test.ts | 2 +- src/tests/promptUndoRedo.test.ts | 2 +- src/tests/session.test.ts | 2 +- src/tests/shell-utils.test.ts | 4 +-- src/tools/bash-handler.ts | 6 ++-- src/tools/edit-handler.ts | 4 +-- src/tools/read-handler.ts | 2 +- src/tools/update-plan-handler.ts | 2 +- src/tools/write-handler.ts | 10 +++++-- src/ui/App.tsx | 8 ++--- src/ui/AskUserQuestionPrompt.tsx | 4 +-- src/ui/PermissionPrompt.tsx | 2 +- src/ui/ProcessStdoutView.tsx | 6 ++-- src/ui/PromptInput.tsx | 30 +++++++++---------- src/ui/SlashCommandMenu.tsx | 4 +-- src/ui/WelcomeScreen.tsx | 2 +- src/ui/components/FileMentionMenu/index.tsx | 2 +- src/ui/{ => core}/askUserQuestion.ts | 2 +- src/ui/{ => core}/clipboard.ts | 0 src/ui/{ => core}/fileMentions.ts | 0 src/ui/{ => core}/loadingText.ts | 2 +- src/ui/{ => core}/promptBuffer.ts | 0 src/ui/{ => core}/promptUndoRedo.ts | 0 src/ui/{ => core}/slashCommands.ts | 2 +- src/ui/{ => core}/thinkingState.ts | 2 +- src/ui/{prompt => hooks}/cursor.ts | 2 +- .../{prompt => hooks}/history-navigation.ts | 2 +- src/ui/{prompt => hooks}/index.ts | 0 src/ui/{prompt => hooks}/paste-handling.ts | 4 +-- src/ui/{prompt => hooks}/useTerminalInput.ts | 0 src/ui/index.ts | 29 ++++++------------ 51 files changed, 108 insertions(+), 113 deletions(-) rename src/common/{ => logging}/debug-logger.ts (100%) rename src/common/{ => logging}/error-logger.ts (100%) rename src/common/{ => runtime}/file-history.ts (100%) rename src/common/{ => runtime}/runtime.ts (98%) rename src/common/{ => runtime}/state.ts (98%) rename src/common/{ => system}/bash-timeout.ts (100%) rename src/common/{ => system}/process-tree.ts (100%) rename src/common/{ => system}/shell-utils.ts (100%) create mode 100644 src/session/utils.ts rename src/ui/{ => core}/askUserQuestion.ts (98%) rename src/ui/{ => core}/clipboard.ts (100%) rename src/ui/{ => core}/fileMentions.ts (100%) rename src/ui/{ => core}/loadingText.ts (96%) rename src/ui/{ => core}/promptBuffer.ts (100%) rename src/ui/{ => core}/promptUndoRedo.ts (100%) rename src/ui/{ => core}/slashCommands.ts (98%) rename src/ui/{ => core}/thinkingState.ts (95%) rename src/ui/{prompt => hooks}/cursor.ts (99%) rename src/ui/{prompt => hooks}/history-navigation.ts (96%) rename src/ui/{prompt => hooks}/index.ts (100%) rename src/ui/{prompt => hooks}/paste-handling.ts (97%) rename src/ui/{prompt => hooks}/useTerminalInput.ts (100%) diff --git a/src/cli.tsx b/src/cli.tsx index d179203e..de26bf2c 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,6 +1,6 @@ import React from "react"; import { render } from "ink"; -import { setShellIfWindows } from "./common/shell-utils"; +import { setShellIfWindows } from "./common/system/shell-utils"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/updateCheck"; import { AppContainer } from "./ui"; diff --git a/src/common/file-utils.ts b/src/common/file-utils.ts index 6656172e..72c83c0a 100644 --- a/src/common/file-utils.ts +++ b/src/common/file-utils.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; import * as path from "path"; -import type { FileState, FileLineEnding } from "./state"; +import type { FileState, FileLineEnding } from "./runtime/state"; export type FileReadMetadata = { content: string; diff --git a/src/common/debug-logger.ts b/src/common/logging/debug-logger.ts similarity index 100% rename from src/common/debug-logger.ts rename to src/common/logging/debug-logger.ts diff --git a/src/common/error-logger.ts b/src/common/logging/error-logger.ts similarity index 100% rename from src/common/error-logger.ts rename to src/common/logging/error-logger.ts diff --git a/src/common/permissions.ts b/src/common/permissions.ts index 564bfeb8..1ebca8c2 100644 --- a/src/common/permissions.ts +++ b/src/common/permissions.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import type { DeepcodingSettings, PermissionScope, PermissionSettings } from "../settings"; -import { isAbsoluteFilePath, normalizeFilePath } from "./state"; +import { isAbsoluteFilePath, normalizeFilePath } from "./runtime/state"; export type BashPermissionScope = Exclude | "unknown"; diff --git a/src/common/file-history.ts b/src/common/runtime/file-history.ts similarity index 100% rename from src/common/file-history.ts rename to src/common/runtime/file-history.ts diff --git a/src/common/runtime.ts b/src/common/runtime/runtime.ts similarity index 98% rename from src/common/runtime.ts rename to src/common/runtime/runtime.ts index b1195d8d..756dc819 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime/runtime.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { ToolExecutionContext, ToolExecutionResult } from "../tools/executor"; +import type { ToolExecutionContext, ToolExecutionResult } from "../../tools/executor"; export type ValidationResult = { ok: true; input: Record } | { ok: false; error: string }; diff --git a/src/common/state.ts b/src/common/runtime/state.ts similarity index 98% rename from src/common/state.ts rename to src/common/runtime/state.ts index add27f35..122a1aca 100644 --- a/src/common/state.ts +++ b/src/common/runtime/state.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { posixPathToWindowsPath } from "./shell-utils"; +import { posixPathToWindowsPath } from "../system/shell-utils"; export type FileLineEnding = "LF" | "CRLF"; diff --git a/src/common/bash-timeout.ts b/src/common/system/bash-timeout.ts similarity index 100% rename from src/common/bash-timeout.ts rename to src/common/system/bash-timeout.ts diff --git a/src/common/process-tree.ts b/src/common/system/process-tree.ts similarity index 100% rename from src/common/process-tree.ts rename to src/common/system/process-tree.ts diff --git a/src/common/shell-utils.ts b/src/common/system/shell-utils.ts similarity index 100% rename from src/common/shell-utils.ts rename to src/common/system/shell-utils.ts diff --git a/src/common/updateCheck.ts b/src/common/updateCheck.ts index 09c0273c..6baa58f7 100644 --- a/src/common/updateCheck.ts +++ b/src/common/updateCheck.ts @@ -6,7 +6,7 @@ import * as path from "path"; import { render, type Instance } from "ink"; import chalk from "chalk"; import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; -import { killProcessTree } from "./process-tree"; +import { killProcessTree } from "./system/process-tree"; export type PackageInfo = { name: string; diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index 26a7a321..4ea0eca4 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -1,7 +1,7 @@ import { spawn, type ChildProcess } from "child_process"; import { createInterface, type Interface } from "readline"; import * as path from "path"; -import { killProcessTree } from "../common/process-tree"; +import { killProcessTree } from "../common/system/process-tree"; type JsonRpcRequest = { jsonrpc: "2.0"; diff --git a/src/prompt.ts b/src/prompt.ts index 4fcd06d6..ce8b6a64 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -5,7 +5,7 @@ import * as path from "path"; import { fileURLToPath } from "url"; import ejs from "ejs"; import type { SessionMessage } from "./session-types"; -import { findGitBashPath, resolveShellPath } from "./common/shell-utils"; +import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; const COMPACT_PROMPT_BASE = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. diff --git a/src/session-types.ts b/src/session-types.ts index 33119cc3..ffc836f7 100644 --- a/src/session-types.ts +++ b/src/session-types.ts @@ -1,7 +1,6 @@ import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; import type { CreateOpenAIClient } from "./tools/executor"; -import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; export type SessionStatus = | "failed" @@ -139,24 +138,3 @@ export type LlmStreamProgress = { formattedTokens: string; phase: "start" | "update" | "end"; }; - -const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; -const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; - -export function getCompactPromptTokenThreshold(model: string): number { - return DEEPSEEK_V4_MODELS.has(model) - ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD - : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; -} - -function isUsageRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -export function getTotalTokens(usage: ModelUsage | null | undefined): number { - if (!isUsageRecord(usage)) { - return 0; - } - const totalTokens = (usage as Record).total_tokens; - return typeof totalTokens === "number" ? totalTokens : 0; -} diff --git a/src/session.ts b/src/session.ts index bf501671..fa881707 100644 --- a/src/session.ts +++ b/src/session.ts @@ -27,11 +27,11 @@ import { } from "./tools/executor"; import { McpManager } from "./mcp/mcp-manager"; import type { McpServerConfig, PermissionSettings } from "./settings"; -import { logApiError } from "./common/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; -import { killProcessTree } from "./common/process-tree"; -import { GitFileHistory } from "./common/file-history"; -import { getSnippet } from "./common/state"; +import { logApiError } from "./common/logging/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/logging/debug-logger"; +import { killProcessTree } from "./common/system/process-tree"; +import { GitFileHistory } from "./common/runtime/file-history"; +import { getSnippet } from "./common/runtime/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, @@ -44,10 +44,9 @@ import { type UserToolPermission, } from "./common/permissions"; +import { getCompactPromptTokenThreshold, getTotalTokens } from "./session/utils"; import { type BashTimeoutAdjustment, - getCompactPromptTokenThreshold, - getTotalTokens, type LlmStreamProgress, type MessageMeta, type ModelUsage, diff --git a/src/session/utils.ts b/src/session/utils.ts new file mode 100644 index 00000000..3860a841 --- /dev/null +++ b/src/session/utils.ts @@ -0,0 +1,23 @@ +import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; +import type { ModelUsage } from "../session-types"; + +const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; +const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; + +export function getCompactPromptTokenThreshold(model: string): number { + return DEEPSEEK_V4_MODELS.has(model) + ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD + : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; +} + +function isUsageRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function getTotalTokens(usage: ModelUsage | null | undefined): number { + if (!isUsageRecord(usage)) { + return 0; + } + const totalTokens = (usage as Record).total_tokens; + return typeof totalTokens === "number" ? totalTokens : 0; +} diff --git a/src/tests/clipboard.test.ts b/src/tests/clipboard.test.ts index dbe9ff95..3ca892eb 100644 --- a/src/tests/clipboard.test.ts +++ b/src/tests/clipboard.test.ts @@ -5,7 +5,7 @@ import * as os from "os"; import * as path from "path"; // eslint-disable-next-line @typescript-eslint/consistent-type-imports -type ClipboardModule = typeof import("../ui/clipboard"); +type ClipboardModule = typeof import("../ui/core/clipboard"); const ORIGINAL_PATH = process.env.PATH; const ORIGINAL_PLATFORM = process.platform; @@ -30,7 +30,7 @@ function withPlatform(platform: NodeJS.Platform, fn: () => T): T { test("readClipboardImage returns null when no clipboard helpers are installed", async () => { // Reload module so it picks up the patched PATH at spawn time. - const moduleUrl = new URL(`../ui/clipboard.ts?t=${Date.now()}`, import.meta.url).href; + const moduleUrl = new URL(`../ui/core/clipboard.ts?t=${Date.now()}`, import.meta.url).href; const { readClipboardImage } = (await import(moduleUrl)) as ClipboardModule; const result = withCleanPath(() => readClipboardImage()); assert.equal(result, null); @@ -63,7 +63,7 @@ test( { mode: 0o755 } ); - const moduleUrl = new URL(`../ui/clipboard.ts?t=${Date.now()}`, import.meta.url).href; + const moduleUrl = new URL(`../ui/core/clipboard.ts?t=${Date.now()}`, import.meta.url).href; const { readClipboardImage } = (await import(moduleUrl)) as ClipboardModule; process.env.PATH = binDir; diff --git a/src/tests/debug-logger.test.ts b/src/tests/debug-logger.test.ts index 7b1aad40..374da743 100644 --- a/src/tests/debug-logger.test.ts +++ b/src/tests/debug-logger.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/debug-logger"; +import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/logging/debug-logger"; test("debug logger appends full entries without rotation", () => { const originalHome = process.env.HOME; diff --git a/src/tests/fileMentions.test.ts b/src/tests/fileMentions.test.ts index b382eeed..50a6dc41 100644 --- a/src/tests/fileMentions.test.ts +++ b/src/tests/fileMentions.test.ts @@ -10,7 +10,7 @@ import { replaceCurrentFileMentionToken, scanFileMentionItems, type FileMentionItem, -} from "../ui/fileMentions"; +} from "../ui/core/fileMentions"; test("getCurrentFileMentionToken detects bare @file tokens under the cursor", () => { assert.deepEqual(getCurrentFileMentionToken({ text: "review @src/app.ts please", cursor: 10 }), { diff --git a/src/tests/process-tree.test.ts b/src/tests/process-tree.test.ts index 1dd08a1e..97c68248 100644 --- a/src/tests/process-tree.test.ts +++ b/src/tests/process-tree.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { killProcessTree, runWindowsTaskkill } from "../common/process-tree"; +import { killProcessTree, runWindowsTaskkill } from "../common/system/process-tree"; test("runWindowsTaskkill invokes taskkill for the full process tree", () => { const calls: Array<{ command: string; args: string[]; options: { stdio: "ignore"; windowsHide: true } }> = []; diff --git a/src/tests/promptUndoRedo.test.ts b/src/tests/promptUndoRedo.test.ts index c1999f15..26360c04 100644 --- a/src/tests/promptUndoRedo.test.ts +++ b/src/tests/promptUndoRedo.test.ts @@ -8,7 +8,7 @@ import { recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "../ui/promptUndoRedo"; +} from "../ui/core/promptUndoRedo"; test("prompt undo and redo restore edited buffer states", () => { const history = createPromptUndoRedoState(); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 5615ff55..bfacd8ef 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { GitFileHistory } from "../common/file-history"; +import { GitFileHistory } from "../common/runtime/file-history"; import { type SessionMessage } from "../session-types"; import { SessionManager } from "../session"; diff --git a/src/tests/shell-utils.test.ts b/src/tests/shell-utils.test.ts index 50a71f41..9eec57b6 100644 --- a/src/tests/shell-utils.test.ts +++ b/src/tests/shell-utils.test.ts @@ -7,8 +7,8 @@ import { resolveWindowsGitBashPath, rewriteWindowsNullRedirect, windowsPathToPosixPath, -} from "../common/shell-utils"; -import { isAbsoluteFilePath, normalizeFilePath } from "../common/state"; +} from "../common/system/shell-utils"; +import { isAbsoluteFilePath, normalizeFilePath } from "../common/runtime/state"; test("Windows paths convert to Git Bash POSIX paths", () => { assert.equal(windowsPathToPosixPath("C:\\Users\\foo"), "/c/Users/foo"); diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts index 42722710..fb639158 100644 --- a/src/tools/bash-handler.ts +++ b/src/tools/bash-handler.ts @@ -1,6 +1,6 @@ import { spawn } from "child_process"; -import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/bash-timeout"; -import { killProcessTree } from "../common/process-tree"; +import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/system/bash-timeout"; +import { killProcessTree } from "../common/system/process-tree"; import type { ProcessTimeoutControl, ProcessTimeoutInfo, ToolExecutionContext, ToolExecutionResult } from "./executor"; import { buildDisableExtglobCommand, @@ -9,7 +9,7 @@ import { resolveShellPath, rewriteWindowsNullRedirect, toNativeCwd, -} from "../common/shell-utils"; +} from "../common/system/shell-utils"; const MAX_OUTPUT_CHARS = 30000; const MAX_CAPTURE_CHARS = 10 * 1024 * 1024; diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index 454a673b..98afa43f 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -8,7 +8,7 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool, semanticBoolean } from "../common/runtime"; +import { executeValidatedTool, semanticBoolean } from "../common/runtime/runtime"; import { createSnippet, getFileState, @@ -18,7 +18,7 @@ import { isFullFileView, normalizeFilePath, recordFileState, -} from "../common/state"; +} from "../common/runtime/state"; const MAX_CANDIDATE_COUNT = 5; const REPLACE_ALL_MATCH_THRESHOLD = 5; diff --git a/src/tools/read-handler.ts b/src/tools/read-handler.ts index 964cdd72..606199c5 100644 --- a/src/tools/read-handler.ts +++ b/src/tools/read-handler.ts @@ -3,7 +3,7 @@ import * as path from "path"; import ignore from "ignore"; import type { ToolExecutionContext, ToolExecutionFollowUpMessage, ToolExecutionResult } from "./executor"; import { readTextFileWithMetadata } from "../common/file-utils"; -import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/state"; +import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/runtime/state"; const DEFAULT_LINE_LIMIT = 2000; const MAX_LINE_LENGTH = 2000; diff --git a/src/tools/update-plan-handler.ts b/src/tools/update-plan-handler.ts index 7c7198ea..a8947cfc 100644 --- a/src/tools/update-plan-handler.ts +++ b/src/tools/update-plan-handler.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { ToolExecutionContext, ToolExecutionResult } from "./executor"; -import { executeValidatedTool } from "../common/runtime"; +import { executeValidatedTool } from "../common/runtime/runtime"; const updatePlanSchema = z.strictObject({ plan: z.string().trim().min(1, "plan must not be empty."), diff --git a/src/tools/write-handler.ts b/src/tools/write-handler.ts index a4c81bf3..e91a78c7 100644 --- a/src/tools/write-handler.ts +++ b/src/tools/write-handler.ts @@ -9,8 +9,14 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool } from "../common/runtime"; -import { getFileState, isAbsoluteFilePath, isFullFileView, normalizeFilePath, recordFileState } from "../common/state"; +import { executeValidatedTool } from "../common/runtime/runtime"; +import { + getFileState, + isAbsoluteFilePath, + isFullFileView, + normalizeFilePath, + recordFileState, +} from "../common/runtime/state"; const writeSchema = z.strictObject({ file_path: z.string().min(1, "file_path is required."), diff --git a/src/ui/App.tsx b/src/ui/App.tsx index b140574d..bea96f66 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -8,8 +8,8 @@ import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptIn import { MessageView, RawModeExitPrompt } from "./components"; import { SessionList } from "./SessionList"; import { type UndoRestoreMode, UndoSelector } from "./UndoSelector"; -import { buildLoadingText } from "./loadingText"; -import { findExpandedThinkingId } from "./thinkingState"; +import { buildLoadingText } from "./core/loadingText"; +import { findExpandedThinkingId } from "./core/thinkingState"; import { WelcomeScreen } from "./WelcomeScreen"; import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; import { McpStatusList } from "./McpStatusList"; @@ -18,7 +18,7 @@ import { type AskUserQuestionAnswers, findPendingAskUserQuestion, formatAskUserQuestionAnswers, -} from "./askUserQuestion"; +} from "./core/askUserQuestion"; import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt"; import { buildExitSummaryText } from "./exitSummary"; import { RawMode, useRawModeContext } from "./contexts"; @@ -32,7 +32,7 @@ import { renderRawModeMessages, } from "./utils"; import { resolveCurrentSettings, writeModelConfigSelection } from "../settings"; -import { isCollapsedThinking } from "./thinkingState"; +import { isCollapsedThinking } from "./core/thinkingState"; import { ANSI_CLEAR_SCREEN } from "./constants"; import type { LlmStreamProgress, diff --git a/src/ui/AskUserQuestionPrompt.tsx b/src/ui/AskUserQuestionPrompt.tsx index c84b6200..058de3e1 100644 --- a/src/ui/AskUserQuestionPrompt.tsx +++ b/src/ui/AskUserQuestionPrompt.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import type { AskUserQuestionAnswers, AskUserQuestionItem } from "./askUserQuestion"; -import { useTerminalInput } from "./prompt"; +import type { AskUserQuestionAnswers, AskUserQuestionItem } from "./core/askUserQuestion"; +import { useTerminalInput } from "./hooks"; type Props = { questions: AskUserQuestionItem[]; diff --git a/src/ui/PermissionPrompt.tsx b/src/ui/PermissionPrompt.tsx index dd2d8ebf..f450ac96 100644 --- a/src/ui/PermissionPrompt.tsx +++ b/src/ui/PermissionPrompt.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import { useTerminalInput } from "./prompt"; +import { useTerminalInput } from "./hooks"; import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../common/permissions"; import type { PermissionScope } from "../settings"; diff --git a/src/ui/ProcessStdoutView.tsx b/src/ui/ProcessStdoutView.tsx index b47e0cdd..23b230aa 100644 --- a/src/ui/ProcessStdoutView.tsx +++ b/src/ui/ProcessStdoutView.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; -import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../common/bash-timeout"; +import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../common/system/bash-timeout"; import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../session-types"; -import { useTerminalInput } from "./prompt"; +import { useTerminalInput } from "./hooks"; type RunningProcesses = SessionEntry["processes"]; type ProcessStdoutViewProps = { - processStdoutRef: React.MutableRefObject>; + processStdoutRef: React.RefObject>; runningProcesses: RunningProcesses; onDismiss: () => void; onAdjustTimeout: (deltaMs: number) => BashTimeoutAdjustment | null; diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index ab8955ef..27af870b 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -24,40 +24,40 @@ import { moveWordLeft, moveWordRight, moveUp, -} from "./promptBuffer"; -import type { PromptBufferState } from "./promptBuffer"; +} from "./core/promptBuffer"; +import type { PromptBufferState } from "./core/promptBuffer"; import { clearPromptUndoRedoState, createPromptUndoRedoState, recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "./promptUndoRedo"; -import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "./slashCommands"; -import type { SlashCommandItem } from "./slashCommands"; +} from "./core/promptUndoRedo"; +import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "./core/slashCommands"; +import type { SlashCommandItem } from "./core/slashCommands"; import { filterFileMentionItems, getCurrentFileMentionToken, replaceCurrentFileMentionToken, scanFileMentionItems, -} from "./fileMentions"; -import type { FileMentionItem } from "./fileMentions"; -import { readClipboardImageAsync } from "./clipboard"; +} from "./core/fileMentions"; +import type { FileMentionItem } from "./core/fileMentions"; +import { readClipboardImageAsync } from "./core/clipboard"; // Re-exported from prompt modules for backward compatibility -export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./prompt"; -export type { InputKey } from "./prompt"; +export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./hooks"; +export type { InputKey } from "./hooks"; -import { useTerminalInput } from "./prompt"; -import type { InputKey } from "./prompt"; -import { usePasteHandling } from "./prompt/paste-handling"; -import { useHistoryNavigation } from "./prompt/history-navigation"; +import { useTerminalInput } from "./hooks"; +import type { InputKey } from "./hooks"; +import { usePasteHandling } from "./hooks/paste-handling"; +import { useHistoryNavigation } from "./hooks/history-navigation"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, useBracketedPaste, useTerminalFocusReporting, -} from "./prompt"; +} from "./hooks"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; import type { ModelConfigSelection, PermissionScope } from "../settings"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/SlashCommandMenu.tsx index ddd79251..0ca5c696 100644 --- a/src/ui/SlashCommandMenu.tsx +++ b/src/ui/SlashCommandMenu.tsx @@ -1,5 +1,5 @@ -import { formatSlashCommandDescription, formatSlashCommandLabel } from "./slashCommands"; -import type { SlashCommandItem } from "./slashCommands"; +import { formatSlashCommandDescription, formatSlashCommandLabel } from "./core/slashCommands"; +import type { SlashCommandItem } from "./core/slashCommands"; import { ARGS_SEPARATOR } from "./constants"; import React from "react"; import { Box, Text } from "ink"; diff --git a/src/ui/WelcomeScreen.tsx b/src/ui/WelcomeScreen.tsx index 2e6b7cfa..36bb3030 100644 --- a/src/ui/WelcomeScreen.tsx +++ b/src/ui/WelcomeScreen.tsx @@ -4,7 +4,7 @@ import * as os from "node:os"; import path from "node:path"; import type { SkillInfo } from "../session-types"; import type { ResolvedDeepcodingSettings } from "../settings"; -import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./slashCommands"; +import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./core/slashCommands"; import { ThemedGradient } from "./ThemedGradient"; import { AsciiLogo } from "./AsciiArt"; import { useAppContext } from "./contexts"; diff --git a/src/ui/components/FileMentionMenu/index.tsx b/src/ui/components/FileMentionMenu/index.tsx index b1c77b4a..15465d42 100644 --- a/src/ui/components/FileMentionMenu/index.tsx +++ b/src/ui/components/FileMentionMenu/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Box, Text } from "ink"; import { useInput } from "ink"; import DropdownMenu from "../DropdownMenu"; -import type { FileMentionItem, FileMentionToken } from "../../fileMentions"; +import type { FileMentionItem, FileMentionToken } from "../../core/fileMentions"; type Props = { open: boolean; diff --git a/src/ui/askUserQuestion.ts b/src/ui/core/askUserQuestion.ts similarity index 98% rename from src/ui/askUserQuestion.ts rename to src/ui/core/askUserQuestion.ts index 813f7cf8..ea94342e 100644 --- a/src/ui/askUserQuestion.ts +++ b/src/ui/core/askUserQuestion.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../session-types"; +import type { SessionMessage, SessionStatus } from "../../session-types"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/clipboard.ts b/src/ui/core/clipboard.ts similarity index 100% rename from src/ui/clipboard.ts rename to src/ui/core/clipboard.ts diff --git a/src/ui/fileMentions.ts b/src/ui/core/fileMentions.ts similarity index 100% rename from src/ui/fileMentions.ts rename to src/ui/core/fileMentions.ts diff --git a/src/ui/loadingText.ts b/src/ui/core/loadingText.ts similarity index 96% rename from src/ui/loadingText.ts rename to src/ui/core/loadingText.ts index 71304055..738b9685 100644 --- a/src/ui/loadingText.ts +++ b/src/ui/core/loadingText.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../session-types"; +import type { LlmStreamProgress, SessionEntry } from "../../session-types"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/promptBuffer.ts b/src/ui/core/promptBuffer.ts similarity index 100% rename from src/ui/promptBuffer.ts rename to src/ui/core/promptBuffer.ts diff --git a/src/ui/promptUndoRedo.ts b/src/ui/core/promptUndoRedo.ts similarity index 100% rename from src/ui/promptUndoRedo.ts rename to src/ui/core/promptUndoRedo.ts diff --git a/src/ui/slashCommands.ts b/src/ui/core/slashCommands.ts similarity index 98% rename from src/ui/slashCommands.ts rename to src/ui/core/slashCommands.ts index 2677a231..d96734b0 100644 --- a/src/ui/slashCommands.ts +++ b/src/ui/core/slashCommands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../session-types"; +import type { SkillInfo } from "../../session-types"; export type SlashCommandKind = | "skill" diff --git a/src/ui/thinkingState.ts b/src/ui/core/thinkingState.ts similarity index 95% rename from src/ui/thinkingState.ts rename to src/ui/core/thinkingState.ts index aad6d212..e7a525a7 100644 --- a/src/ui/thinkingState.ts +++ b/src/ui/core/thinkingState.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../session-types"; +import type { SessionMessage } from "../../session-types"; /** * Returns the message id of the assistant "thinking" message that should stay diff --git a/src/ui/prompt/cursor.ts b/src/ui/hooks/cursor.ts similarity index 99% rename from src/ui/prompt/cursor.ts rename to src/ui/hooks/cursor.ts index aefea342..2ecbddd7 100644 --- a/src/ui/prompt/cursor.ts +++ b/src/ui/hooks/cursor.ts @@ -1,5 +1,5 @@ import { useLayoutEffect, useRef } from "react"; -import type { PromptBufferState } from "../promptBuffer"; +import type { PromptBufferState } from "../core/promptBuffer"; type CursorPlacement = { rowsUp: number; diff --git a/src/ui/prompt/history-navigation.ts b/src/ui/hooks/history-navigation.ts similarity index 96% rename from src/ui/prompt/history-navigation.ts rename to src/ui/hooks/history-navigation.ts index 22ad0f63..1f595a9c 100644 --- a/src/ui/prompt/history-navigation.ts +++ b/src/ui/hooks/history-navigation.ts @@ -1,6 +1,6 @@ import type React from "react"; import { useCallback, useState } from "react"; -import type { PromptBufferState } from "../promptBuffer"; +import type { PromptBufferState } from "../core/promptBuffer"; export type HistoryNavigationState = { historyCursor: number; diff --git a/src/ui/prompt/index.ts b/src/ui/hooks/index.ts similarity index 100% rename from src/ui/prompt/index.ts rename to src/ui/hooks/index.ts diff --git a/src/ui/prompt/paste-handling.ts b/src/ui/hooks/paste-handling.ts similarity index 97% rename from src/ui/prompt/paste-handling.ts rename to src/ui/hooks/paste-handling.ts index 63c2c547..1ecdd3d1 100644 --- a/src/ui/prompt/paste-handling.ts +++ b/src/ui/hooks/paste-handling.ts @@ -1,7 +1,7 @@ import type React from "react"; import { useRef, useState } from "react"; -import type { PromptBufferState } from "../promptBuffer"; -import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../promptBuffer"; +import type { PromptBufferState } from "../core/promptBuffer"; +import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../core/promptBuffer"; export type PasteRegion = { start: number; diff --git a/src/ui/prompt/useTerminalInput.ts b/src/ui/hooks/useTerminalInput.ts similarity index 100% rename from src/ui/prompt/useTerminalInput.ts rename to src/ui/hooks/useTerminalInput.ts diff --git a/src/ui/index.ts b/src/ui/index.ts index f3cd41a7..482e59f9 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -4,18 +4,9 @@ import { MODEL_COMMAND_THINKING_OPTIONS, } from "./components/ModelsDropdown"; -export { - readSettings, - readProjectSettings, - writeSettings, - writeProjectSettings, - writeModelConfigSelection, - resolveCurrentSettings, - DEFAULT_MODEL, - DEFAULT_BASE_URL, -} from "../settings"; +export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; export { buildPromptDraftFromSessionMessage } from "./utils"; -export { createOpenAIClient } from "../common/openai-client"; +export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./hooks/cursor"; export { default as AppContainer } from "./AppContainer"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; export { MessageView } from "./components"; @@ -39,8 +30,6 @@ export { type PromptDraft, type InputKey, } from "./PromptInput"; -export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; -export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./prompt/cursor"; export { SessionList, formatSessionTitle, filterSessions, formatSessionStatus } from "./SessionList"; export { ThemedGradient } from "./ThemedGradient"; export { UpdatePrompt, type UpdatePromptChoice } from "./UpdatePrompt"; @@ -53,9 +42,9 @@ export { type AskUserQuestionItem, type PendingAskUserQuestion, type AskUserQuestionAnswers, -} from "./askUserQuestion"; -export { readClipboardImage, type ClipboardImage } from "./clipboard"; -export { buildLoadingText, type LoadingTextInput } from "./loadingText"; +} from "./core/askUserQuestion"; +export { readClipboardImage, type ClipboardImage } from "./core/clipboard"; +export { buildLoadingText, type LoadingTextInput } from "./core/loadingText"; export { renderMarkdown, renderMarkdownSegments, type MarkdownSegment } from "./components/MessageView/markdown"; export { EMPTY_BUFFER, @@ -77,7 +66,7 @@ export { isEmpty, getCurrentSlashToken, type PromptBufferState, -} from "./promptBuffer"; +} from "./core/promptBuffer"; export { BUILTIN_SLASH_COMMANDS, buildSlashCommands, @@ -87,7 +76,7 @@ export { formatSlashCommandLabel, type SlashCommandKind, type SlashCommandItem, -} from "./slashCommands"; +} from "./core/slashCommands"; export { filterFileMentionItems, formatFileMentionPath, @@ -96,6 +85,6 @@ export { scanFileMentionItems, type FileMentionItem, type FileMentionToken, -} from "./fileMentions"; -export { findExpandedThinkingId, isCollapsedThinking } from "./thinkingState"; +} from "./core/fileMentions"; +export { findExpandedThinkingId, isCollapsedThinking } from "./core/thinkingState"; export { buildExitSummaryText } from "./exitSummary"; From 0b624607d1eb32622a496da8e6b43ed70fa64e28 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 25 May 2026 18:27:22 +0800 Subject: [PATCH 080/212] feat: enhance snippet handling with new full-file support and required snippet_id --- src/common/state.ts | 34 +++++++++++++-- src/prompt.ts | 13 +++--- src/tests/tool-handlers.test.ts | 74 ++++++++++++++++++++++----------- src/tools/edit-handler.ts | 35 ++++------------ src/tools/read-handler.ts | 18 ++++---- templates/tools/edit.md | 16 +++---- templates/tools/read.md.ejs | 2 +- 7 files changed, 114 insertions(+), 78 deletions(-) diff --git a/src/common/state.ts b/src/common/state.ts index add27f35..d080571c 100644 --- a/src/common/state.ts +++ b/src/common/state.ts @@ -22,11 +22,13 @@ export type FileSnippet = { endLine: number; preview: string; fileVersion: number; + scopeType: "snippet" | "full"; }; const fileStatesBySession = new Map>(); const snippetsBySession = new Map>(); const snippetCountersBySession = new Map(); +const fullFileSnippetCountersBySession = new Map(); const fileVersionsBySession = new Map>(); export function normalizeFilePath(filePath: string, platform: NodeJS.Platform = process.platform): string { @@ -147,21 +149,45 @@ export function createSnippet( startLine: number, endLine: number, preview: string +): FileSnippet | null { + const nextCounter = (snippetCountersBySession.get(sessionId) ?? 0) + 1; + snippetCountersBySession.set(sessionId, nextCounter); + return createSnippetWithId(sessionId, filePath, startLine, endLine, preview, `snippet_${nextCounter}`, "snippet"); +} + +export function createFullFileSnippet( + sessionId: string, + filePath: string, + startLine: number, + endLine: number, + preview: string +): FileSnippet | null { + const nextCounter = fullFileSnippetCountersBySession.get(sessionId) ?? 0; + fullFileSnippetCountersBySession.set(sessionId, nextCounter + 1); + return createSnippetWithId(sessionId, filePath, startLine, endLine, preview, `full_file_${nextCounter}`, "full"); +} + +function createSnippetWithId( + sessionId: string, + filePath: string, + startLine: number, + endLine: number, + preview: string, + id: string, + scopeType: FileSnippet["scopeType"] ): FileSnippet | null { if (!sessionId || !filePath || startLine < 1 || endLine < startLine) { return null; } - const nextCounter = (snippetCountersBySession.get(sessionId) ?? 0) + 1; - snippetCountersBySession.set(sessionId, nextCounter); - const snippet: FileSnippet = { - id: `snippet_${nextCounter}`, + id, filePath: normalizeFilePath(filePath), startLine, endLine, preview, fileVersion: getFileVersion(sessionId, filePath), + scopeType, }; let snippets = snippetsBySession.get(sessionId); diff --git a/src/prompt.ts b/src/prompt.ts index ba9bf231..e9006d91 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -494,18 +494,17 @@ export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDe parameters: { type: "object", properties: { - file_path: { + snippet_id: { type: "string", - description: "Absolute path to file. Optional when snippet_id is provided.", + description: "Required Read/Edit snippet_id.", }, - snippet_id: { + file_path: { type: "string", - description: - "Snippet id returned by the Read or Edit tool to scope the search range after a partial read.", + description: "Optional absolute path guard; must match snippet_id's file.", }, old_string: { type: "string", - description: "Exact text to replace inside the file or snippet scope", + description: "Exact text to replace inside snippet_id's scope", }, new_string: { type: "string", @@ -521,7 +520,7 @@ export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDe description: "Expected number of matches, especially useful as a safety check with replace_all", }, }, - required: ["old_string", "new_string"], + required: ["snippet_id", "old_string", "new_string"], additionalProperties: false, }, }, diff --git a/src/tests/tool-handlers.test.ts b/src/tests/tool-handlers.test.ts index f66153c5..da1cb9c9 100644 --- a/src/tests/tool-handlers.test.ts +++ b/src/tests/tool-handlers.test.ts @@ -167,17 +167,29 @@ test("Read returns snippet metadata and Edit can scope replacements by snippet_i ); }); +test("Read returns full-file snippet ids with a semantic prefix", async () => { + const workspace = createTempWorkspace(); + const filePath = path.join(workspace, "full.txt"); + fs.writeFileSync(filePath, "alpha\nbeta\n", "utf8"); + + const firstSnippet = await readSnippet(filePath, "full-file-snippet", workspace); + const secondSnippet = await readSnippet(filePath, "full-file-snippet", workspace); + + assert.equal(firstSnippet.id, "full_file_0"); + assert.equal(secondSnippet.id, "full_file_1"); +}); + test("Edit returns candidate match snippets when old_string is not unique", async () => { const workspace = createTempWorkspace(); const filePath = path.join(workspace, "duplicate.txt"); fs.writeFileSync(filePath, ["city", "city", "salary"].join("\n"), "utf8"); const sessionId = "candidate-matches"; - await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + const snippet = await readSnippet(filePath, sessionId, workspace); const editResult = await handleEditTool( { - file_path: filePath, + snippet_id: snippet.id, old_string: "city", new_string: "location", }, @@ -214,11 +226,11 @@ test("Edit returns closest matches only above threshold with surrounding context ); const sessionId = "closest-match-context"; - await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + const fullSnippet = await readSnippet(filePath, sessionId, workspace); const closeResult = await handleEditTool( { - file_path: filePath, + snippet_id: fullSnippet.id, old_string: "function computeTotal(value: number) {", new_string: "function computeTotal(input: number) {", }, @@ -239,7 +251,7 @@ test("Edit returns closest matches only above threshold with surrounding context const lowResult = await handleEditTool( { - file_path: filePath, + snippet_id: fullSnippet.id, old_string: 'query: string = Field(description="search query")', new_string: "query: string", }, @@ -383,11 +395,11 @@ test("replace_all requires expected_occurrences for broad short-fragment replace fs.writeFileSync(filePath, [fragment, fragment, fragment].join("\n---\n"), "utf8"); const sessionId = "replace-all-guard"; - await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + const snippet = await readSnippet(filePath, sessionId, workspace); const blockedResult = await handleEditTool( { - file_path: filePath, + snippet_id: snippet.id, old_string: fragment, new_string: " schema:\n type: array", replace_all: true, @@ -400,7 +412,7 @@ test("replace_all requires expected_occurrences for broad short-fragment replace const allowedResult = await handleEditTool( { - file_path: filePath, + snippet_id: snippet.id, old_string: fragment, new_string: " schema:\n type: array", replace_all: true, @@ -426,11 +438,11 @@ test("Edit accepts a unique loose-escape match when only escaping differs", asyn fs.writeFileSync(filePath, "params['city_json'] = f'\"{city}\"'\n", "utf8"); const sessionId = "closest-match"; - await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + const snippet = await readSnippet(filePath, sessionId, workspace); const editResult = await handleEditTool( { - file_path: filePath, + snippet_id: snippet.id, old_string: "params['city_json'] = f'\\\\\"{city}\\\\\"'", new_string: "params['city_json'] = city", }, @@ -472,12 +484,12 @@ test("Edit accepts a unique loose-escape match for over-escaped unicode sequence fs.writeFileSync(filePath, 'const sequence = "\\u001B[13;2~";\n', "utf8"); const sessionId = "unicode-loose-escape"; - await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + const snippet = await readSnippet(filePath, sessionId, workspace); let llmCalls = 0; const editResult = await handleEditTool( { - file_path: filePath, + snippet_id: snippet.id, old_string: 'const sequence = "\\\\u001B[13;2~";', new_string: 'const sequence = "\\\\u001B[13;130u";', }, @@ -524,11 +536,11 @@ test("Edit strips accidental read-result tabs after newlines when that creates a fs.writeFileSync(filePath, ["function demo() {", " return 1;", "}"].join("\n") + "\n", "utf8"); const sessionId = "line-leading-tab-correction"; - await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + const snippet = await readSnippet(filePath, sessionId, workspace); const editResult = await handleEditTool( { - file_path: filePath, + snippet_id: snippet.id, old_string: "function demo() {\n\t return 1;\n\t}", new_string: "function demo() {\n\t return 2;\n\t}", }, @@ -565,7 +577,7 @@ test("Write repairs JSON object content for .json files", async () => { assert.equal(fs.readFileSync(filePath, "utf8"), '{\n "name": "demo",\n "private": true\n}'); }); -test("Write updates file state so a follow-up Edit can succeed without another Read", async () => { +test("Edit requires snippet_id even after Write refreshes file state", async () => { const workspace = createTempWorkspace(); const filePath = path.join(workspace, "note.txt"); @@ -590,11 +602,9 @@ test("Write updates file state so a follow-up Edit can succeed without another R createContext("write-then-edit", workspace) ); - assert.equal(editResult.ok, true); - assert.equal(editResult.metadata?.read_scope_type, "full"); - assert.match(String(editResult.metadata?.diff_preview ?? ""), /-beta/); - assert.match(String(editResult.metadata?.diff_preview ?? ""), /\+gamma/); - assert.equal(fs.readFileSync(filePath, "utf8"), "alpha\ngamma\n"); + assert.equal(editResult.ok, false); + assert.match(editResult.error ?? "", /snippet_id/); + assert.equal(fs.readFileSync(filePath, "utf8"), "alpha\nbeta\n"); }); test("Write requires a full read before overwriting an existing file", async () => { @@ -643,7 +653,7 @@ test("Edit rejects stale reads after the file changes on disk", async () => { fs.writeFileSync(filePath, "before\n", "utf8"); const sessionId = "stale-edit"; - await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + const snippet = await readSnippet(filePath, sessionId, workspace); fs.writeFileSync(filePath, "after\n", "utf8"); const futureTime = new Date(Date.now() + 2000); @@ -651,7 +661,7 @@ test("Edit rejects stale reads after the file changes on disk", async () => { const editResult = await handleEditTool( { - file_path: filePath, + snippet_id: snippet.id, old_string: "after", new_string: "final", }, @@ -685,11 +695,11 @@ test("Edit preserves CRLF line endings for existing files", async () => { fs.writeFileSync(filePath, "alpha\r\nbeta\r\n", "utf8"); const sessionId = "crlf-edit"; - await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + const snippet = await readSnippet(filePath, sessionId, workspace); const editResult = await handleEditTool( { - file_path: filePath, + snippet_id: snippet.id, old_string: "beta", new_string: "gamma", }, @@ -758,6 +768,22 @@ function createTempWorkspace(): string { return dir; } +async function readSnippet( + filePath: string, + sessionId: string, + workspace: string +): Promise<{ id: string; startLine: number; endLine: number }> { + const readResult = await handleReadTool({ file_path: filePath }, createContext(sessionId, workspace)); + assert.equal(readResult.ok, true); + const snippet = (readResult.metadata?.snippet ?? null) as { + id: string; + startLine: number; + endLine: number; + } | null; + assert.ok(snippet); + return snippet; +} + async function waitFor(predicate: () => boolean, timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index 454a673b..7983a5f1 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -15,7 +15,6 @@ import { getSnippet, hasSnippetOutdatedFileVersion, isAbsoluteFilePath, - isFullFileView, normalizeFilePath, recordFileState, } from "../common/state"; @@ -69,7 +68,7 @@ type CorrectedEditStrings = { const editSchema = z.strictObject({ file_path: z.string().optional(), - snippet_id: z.string().optional(), + snippet_id: z.string().min(1, "snippet_id is required."), old_string: z.string(), new_string: z.string(), replace_all: semanticBoolean(false).optional(), @@ -94,19 +93,19 @@ export async function handleEditTool( args, context, async (input) => { - const snippetId = input.snippet_id?.trim() ?? ""; - const snippet = snippetId ? getSnippet(context.sessionId, snippetId) : null; + const snippetId = input.snippet_id.trim(); + const snippet = getSnippet(context.sessionId, snippetId); let filePath = input.file_path?.trim() ?? ""; - if (!filePath && !snippet) { + if (!snippet) { return { ok: false, name: "edit", - error: 'Missing required "file_path" string or "snippet_id" string.', + error: `Unknown snippet_id: ${snippetId}`, }; } - if (!filePath && snippet) { + if (!filePath) { filePath = snippet.filePath; } @@ -119,15 +118,7 @@ export async function handleEditTool( }; } - if (snippetId && !snippet) { - return { - ok: false, - name: "edit", - error: `Unknown snippet_id: ${snippetId}`, - }; - } - - if (snippet && snippet.filePath !== filePath) { + if (snippet.filePath !== filePath) { return { ok: false, name: "edit", @@ -188,14 +179,6 @@ export async function handleEditTool( }; } - if (!snippet && !isFullFileView(fileState)) { - return { - ok: false, - name: "edit", - error: "File was only partially read. Use snippet_id or read the full file before editing.", - }; - } - if (hasFileChangedSinceState(filePath, fileState)) { return { ok: false, @@ -211,7 +194,7 @@ export async function handleEditTool( const newString = input.new_string; const replaceAll = input.replace_all ?? false; const lineIndex = buildLineIndex(raw); - const scope = buildSearchScope(filePath, raw, lineIndex, snippet ?? null); + const scope = buildSearchScope(filePath, raw, lineIndex, snippet); let matches = findOccurrences(raw, oldString, scope); let matchedVia: "exact" | "line_leading_tab_correction" | "loose_escape" | "llm_escape_correction" = "exact"; let replacementOldString = oldString; @@ -346,7 +329,7 @@ export async function handleEditTool( replaced_count: replacedCount, matched_via: matchedVia, cache_refreshed: true, - read_scope_type: snippet ? "snippet" : "full", + read_scope_type: snippet.scopeType, encoding: freshMetadata.encoding, line_endings: freshMetadata.lineEndings, diff_preview: diffPreview, diff --git a/src/tools/read-handler.ts b/src/tools/read-handler.ts index 964cdd72..3771a7e0 100644 --- a/src/tools/read-handler.ts +++ b/src/tools/read-handler.ts @@ -3,7 +3,13 @@ import * as path from "path"; import ignore from "ignore"; import type { ToolExecutionContext, ToolExecutionFollowUpMessage, ToolExecutionResult } from "./executor"; import { readTextFileWithMetadata } from "../common/file-utils"; -import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/state"; +import { + createFullFileSnippet, + createSnippet, + isAbsoluteFilePath, + markFileRead, + normalizeFilePath, +} from "../common/state"; const DEFAULT_LINE_LIMIT = 2000; const MAX_LINE_LENGTH = 2000; @@ -249,13 +255,9 @@ export async function handleReadTool( encoding: textResult.encoding, lineEndings: textResult.lineEndings, }); - const snippet = createSnippet( - context.sessionId, - filePath, - textResult.startLine, - textResult.endLine, - textResult.output - ); + const snippet = textResult.isPartialView + ? createSnippet(context.sessionId, filePath, textResult.startLine, textResult.endLine, textResult.output) + : createFullFileSnippet(context.sessionId, filePath, textResult.startLine, textResult.endLine, textResult.output); return { ok: true, name: "read", diff --git a/templates/tools/edit.md b/templates/tools/edit.md index 4ed48940..efa8574d 100644 --- a/templates/tools/edit.md +++ b/templates/tools/edit.md @@ -3,10 +3,9 @@ Performs scoped string replacements in files. Usage: -- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. -- If your prior Read only covered part of the file, use the returned `snippet_id` to scope the edit, or read the full file before editing without a snippet. +- You must use `Read` tool at least once in the conversation before editing to get the required `snippet_id`. This tool will error if you attempt an edit without reading the file. +- `snippet_id` defines the search scope. Provide `file_path` only as an optional guard that the snippet belongs to the expected file. - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string. -- Prefer passing `snippet_id` from a prior Read response when you want to limit the replacement to a known range. - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked. - If `old_string` is not unique, the tool returns candidate matches with line ranges, previews, and snippet ids that you can reuse in a follow-up edit. @@ -18,16 +17,16 @@ Usage: "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { - "file_path": { - "description": "The absolute path to the file to modify (must be absolute, not relative). Optional when snippet_id is provided.", + "snippet_id": { + "description": "Required snippet_id returned by Read or a prior Edit error response.", "type": "string" }, - "snippet_id": { - "description": "Snippet id returned by Read or a prior Edit error response. Limits the search range to that snippet.", + "file_path": { + "description": "Optional absolute path guard. If provided, it must match the snippet's file.", "type": "string" }, "old_string": { - "description": "The text to replace within the file or snippet scope", + "description": "The text to replace within the snippet_id scope", "type": "string" }, "new_string": { @@ -45,6 +44,7 @@ Usage: } }, "required": [ + "snippet_id", "old_string", "new_string" ], diff --git a/templates/tools/read.md.ejs b/templates/tools/read.md.ejs index a9c50e5f..9f79ac9b 100644 --- a/templates/tools/read.md.ejs +++ b/templates/tools/read.md.ejs @@ -9,7 +9,7 @@ Usage: - You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters - Any lines longer than 2000 characters will be truncated - Results are returned using cat -n format, with line numbers starting at 1 -- Text reads return a snippet id in metadata. You can pass that snippet id to the Edit tool to constrain replacements to just that read range. +- Text reads return a snippet id for Edit: full-file reads use ids like `full_file_0`; partial reads use ids like `snippet_1`. <%_ if (supportsMultimodal) { _%> - This tool allows you to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Deepseek is a multimodal LLM. <%_ } else { _%> From 2dd9794ffa3bf3f7494c4e265c4cb1f0a1b7f083 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 20:24:27 +0800 Subject: [PATCH 081/212] =?UTF-8?q?refactor(ui):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=EF=BC=8C=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=B7=AF=E5=BE=84=E5=92=8C=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将多个 UI 组件和模块从 src/ui 目录移动到 src/ui/views 目录 - 更新所有相关文件中的导入路径,确保引用正确 - 修改多个类型导入路径,从 session-types 改为 session/types - 统一调整 hooks 和核心模块的导入路径,修正导入命名和位置 - 维护代码一致性,避免因路径更改导致的引用错误 - 重命名 session.ts 为 session/index.ts,并修改相关导入 - 更新测试用例中的类型导入路径以匹配新的文件结构 - 将多处核心或公共模块导入路径调整为模块根目录的对应位置 --- src/prompt.ts | 2 +- src/{session.ts => session/index.ts} | 34 ++++++++-------- src/{session-types.ts => session/types.ts} | 6 +-- src/session/utils.ts | 2 +- src/tests/askUserQuestion.test.ts | 2 +- src/tests/exitSummary.test.ts | 2 +- src/tests/messageView.test.ts | 2 +- src/tests/permission-prompt.test.ts | 2 +- src/tests/promptInputKeys.test.ts | 5 +-- src/tests/session.test.ts | 2 +- src/tests/sessionList.test.ts | 2 +- src/tests/slashCommands.test.ts | 2 +- src/tests/thinkingState.test.ts | 2 +- src/ui/components/MessageView/types.ts | 2 +- src/ui/components/MessageView/utils.ts | 2 +- src/ui/components/SkillsDropdown/index.tsx | 4 +- src/ui/core/askUserQuestion.ts | 2 +- src/ui/core/loadingText.ts | 2 +- src/ui/core/slashCommands.ts | 2 +- src/ui/core/thinkingState.ts | 2 +- src/ui/exitSummary.ts | 2 +- src/ui/hooks/index.ts | 8 ++-- ...-navigation.ts => useHistoryNavigation.ts} | 0 ...{paste-handling.ts => usePasteHandling.ts} | 0 src/ui/index.ts | 18 ++++----- src/ui/utils/index.ts | 4 +- src/ui/{ => views}/App.tsx | 32 +++++++-------- src/ui/{ => views}/AppContainer.tsx | 4 +- src/ui/{ => views}/AskUserQuestionPrompt.tsx | 4 +- src/ui/{ => views}/McpStatusList.tsx | 2 +- src/ui/{ => views}/PermissionPrompt.tsx | 6 +-- src/ui/{ => views}/ProcessStdoutView.tsx | 6 +-- src/ui/{ => views}/PromptInput.tsx | 39 ++++++++----------- src/ui/{ => views}/SessionList.tsx | 4 +- src/ui/{ => views}/SlashCommandMenu.tsx | 8 ++-- src/ui/{ => views}/ThemedGradient.tsx | 0 src/ui/{ => views}/UndoSelector.tsx | 2 +- src/ui/{ => views}/UpdatePrompt.tsx | 0 src/ui/{ => views}/WelcomeScreen.tsx | 10 ++--- 39 files changed, 109 insertions(+), 121 deletions(-) rename src/{session.ts => session/index.ts} (99%) rename src/{session-types.ts => session/types.ts} (96%) rename src/ui/hooks/{history-navigation.ts => useHistoryNavigation.ts} (100%) rename src/ui/hooks/{paste-handling.ts => usePasteHandling.ts} (100%) rename src/ui/{ => views}/App.tsx (97%) rename src/ui/{ => views}/AppContainer.tsx (85%) rename src/ui/{ => views}/AskUserQuestionPrompt.tsx (99%) rename src/ui/{ => views}/McpStatusList.tsx (99%) rename src/ui/{ => views}/PermissionPrompt.tsx (98%) rename src/ui/{ => views}/ProcessStdoutView.tsx (98%) rename src/ui/{ => views}/PromptInput.tsx (96%) rename src/ui/{ => views}/SessionList.tsx (98%) rename src/ui/{ => views}/SlashCommandMenu.tsx (93%) rename src/ui/{ => views}/ThemedGradient.tsx (100%) rename src/ui/{ => views}/UndoSelector.tsx (99%) rename src/ui/{ => views}/UpdatePrompt.tsx (100%) rename src/ui/{ => views}/WelcomeScreen.tsx (94%) diff --git a/src/prompt.ts b/src/prompt.ts index ce8b6a64..f4e76d9d 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -4,7 +4,7 @@ import * as os from "os"; import * as path from "path"; import { fileURLToPath } from "url"; import ejs from "ejs"; -import type { SessionMessage } from "./session-types"; +import type { SessionMessage } from "./session/types"; import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; diff --git a/src/session.ts b/src/session/index.ts similarity index 99% rename from src/session.ts rename to src/session/index.ts index fa881707..704e7692 100644 --- a/src/session.ts +++ b/src/session/index.ts @@ -6,9 +6,9 @@ import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; -import { launchNotifyScript } from "./common/notify"; -import { buildThinkingRequestOptions } from "./common/openai-thinking"; -import { supportsMultimodal } from "./common/model-capabilities"; +import { launchNotifyScript } from "../common/notify"; +import { buildThinkingRequestOptions } from "../common/openai-thinking"; +import { supportsMultimodal } from "../common/model-capabilities"; import { getCompactPrompt, getDefaultSkillPrompt, @@ -16,7 +16,7 @@ import { getSystemPrompt, getTools, type ToolDefinition, -} from "./prompt"; +} from "../prompt"; import { type CreateOpenAIClient, type ProcessTimeoutControl, @@ -24,14 +24,14 @@ import { type ToolCallExecution, type ToolExecutionHooks, ToolExecutor, -} from "./tools/executor"; -import { McpManager } from "./mcp/mcp-manager"; -import type { McpServerConfig, PermissionSettings } from "./settings"; -import { logApiError } from "./common/logging/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/logging/debug-logger"; -import { killProcessTree } from "./common/system/process-tree"; -import { GitFileHistory } from "./common/runtime/file-history"; -import { getSnippet } from "./common/runtime/state"; +} from "../tools/executor"; +import { McpManager } from "../mcp/mcp-manager"; +import type { McpServerConfig, PermissionSettings } from "../settings"; +import { logApiError } from "../common/logging/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "../common/logging/debug-logger"; +import { killProcessTree } from "../common/system/process-tree"; +import { GitFileHistory } from "../common/runtime/file-history"; +import { getSnippet } from "../common/runtime/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, @@ -42,9 +42,9 @@ import { parseToolCallForPermissions, type PermissionToolCall, type UserToolPermission, -} from "./common/permissions"; +} from "../common/permissions"; -import { getCompactPromptTokenThreshold, getTotalTokens } from "./session/utils"; +import { getCompactPromptTokenThreshold, getTotalTokens } from "./utils"; import { type BashTimeoutAdjustment, type LlmStreamProgress, @@ -59,7 +59,7 @@ import { type SkillInfo, type UndoTarget, type UserPromptContent, -} from "./session-types"; +} from "./types"; const MAX_SESSION_ENTRIES = 50; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; @@ -135,11 +135,11 @@ function accumulateUsagePerModel( function getExtensionRoot(): string { if (typeof __dirname !== "undefined") { - return path.resolve(__dirname, ".."); + return path.resolve(__dirname, "../.."); } const currentFilePath = fileURLToPath(import.meta.url); - return path.resolve(path.dirname(currentFilePath), ".."); + return path.resolve(path.dirname(currentFilePath), "../.."); } export class SessionManager { diff --git a/src/session-types.ts b/src/session/types.ts similarity index 96% rename from src/session-types.ts rename to src/session/types.ts index ffc836f7..46639c01 100644 --- a/src/session-types.ts +++ b/src/session/types.ts @@ -1,6 +1,6 @@ -import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; -import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; -import type { CreateOpenAIClient } from "./tools/executor"; +import type { McpServerConfig, PermissionScope, PermissionSettings } from "../settings"; +import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "../common/permissions"; +import type { CreateOpenAIClient } from "../tools/executor"; export type SessionStatus = | "failed" diff --git a/src/session/utils.ts b/src/session/utils.ts index 3860a841..3b807002 100644 --- a/src/session/utils.ts +++ b/src/session/utils.ts @@ -1,5 +1,5 @@ import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; -import type { ModelUsage } from "../session-types"; +import type { ModelUsage } from "./types"; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; diff --git a/src/tests/askUserQuestion.test.ts b/src/tests/askUserQuestion.test.ts index 89907b07..10c9a2cb 100644 --- a/src/tests/askUserQuestion.test.ts +++ b/src/tests/askUserQuestion.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, formatAskUserQuestionDecline } from "../ui"; -import type { SessionMessage } from "../session-types"; +import type { SessionMessage } from "../session/types"; function message(content: unknown): SessionMessage { const now = "2026-04-29T00:00:00.000Z"; diff --git a/src/tests/exitSummary.test.ts b/src/tests/exitSummary.test.ts index 651f8b96..e22a904c 100644 --- a/src/tests/exitSummary.test.ts +++ b/src/tests/exitSummary.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildExitSummaryText } from "../ui"; -import type { ModelUsage, SessionEntry } from "../session-types"; +import type { ModelUsage, SessionEntry } from "../session/types"; const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, ""); diff --git a/src/tests/messageView.test.ts b/src/tests/messageView.test.ts index c9dfa8b3..9acd01ed 100644 --- a/src/tests/messageView.test.ts +++ b/src/tests/messageView.test.ts @@ -8,7 +8,7 @@ import { parseToolPayload, } from "../ui/components/MessageView/utils"; import { RawMode } from "../ui/contexts"; -import type { SessionMessage } from "../session-types"; +import type { SessionMessage } from "../session/types"; import type { ToolSummary } from "../ui/components/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { diff --git a/src/tests/permission-prompt.test.ts b/src/tests/permission-prompt.test.ts index aa4f372d..4f1d87e9 100644 --- a/src/tests/permission-prompt.test.ts +++ b/src/tests/permission-prompt.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { getScopeRiskColor } from "../ui/PermissionPrompt"; +import { getScopeRiskColor } from "../ui/views/PermissionPrompt"; test("getScopeRiskColor maps permission scopes by risk", () => { assert.equal(getScopeRiskColor("read-in-cwd"), "#22c55e"); diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts index 3b24b213..6e697b65 100644 --- a/src/tests/promptInputKeys.test.ts +++ b/src/tests/promptInputKeys.test.ts @@ -14,20 +14,19 @@ import { getPromptCursorPlacement, getPromptReturnKeyAction, isClearImageAttachmentsShortcut, - parseTerminalInput, removeCurrentSlashToken, toggleSkillSelection, renderBufferWithCursor, buildInitPromptSubmission, buildPromptDraftFromSessionMessage, - dispatchTerminalInput, disableTerminalExtendedKeys, enableTerminalExtendedKeys, EMPTY_BUFFER, insertText, backspace, } from "../ui"; -import type { SessionMessage, SkillInfo } from "../session-types"; +import type { SessionMessage, SkillInfo } from "../session/types"; +import { dispatchTerminalInput, parseTerminalInput } from "../ui/hooks"; function collectDispatchedInput(data: string) { const events: ReturnType[] = []; diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index bfacd8ef..fd08c4d8 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -5,7 +5,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/runtime/file-history"; -import { type SessionMessage } from "../session-types"; +import { type SessionMessage } from "../session/types"; import { SessionManager } from "../session"; const originalFetch = globalThis.fetch; diff --git a/src/tests/sessionList.test.ts b/src/tests/sessionList.test.ts index edae36b2..5fdda393 100644 --- a/src/tests/sessionList.test.ts +++ b/src/tests/sessionList.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { formatSessionTitle, filterSessions, formatSessionStatus } from "../ui"; -import type { SessionEntry } from "../session-types"; +import type { SessionEntry } from "../session/types"; test("formatSessionTitle replaces newlines with spaces", () => { assert.equal(formatSessionTitle("first line\nsecond line\r\nthird"), "first line second line third"); diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts index d352f3a5..fa98b9f2 100644 --- a/src/tests/slashCommands.test.ts +++ b/src/tests/slashCommands.test.ts @@ -7,7 +7,7 @@ import { formatSlashCommandDescription, formatSlashCommandLabel, } from "../ui"; -import type { SkillInfo } from "../session-types"; +import type { SkillInfo } from "../session/types"; const skills: SkillInfo[] = [ { name: "skill-writer", path: "~/.agents/skills/skill-writer/SKILL.md", description: "Write a SKILL.md" }, diff --git a/src/tests/thinkingState.test.ts b/src/tests/thinkingState.test.ts index f50ab935..347ac571 100644 --- a/src/tests/thinkingState.test.ts +++ b/src/tests/thinkingState.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findExpandedThinkingId } from "../ui"; -import type { SessionMessage } from "../session-types"; +import type { SessionMessage } from "../session/types"; function buildMessage( id: string, diff --git a/src/ui/components/MessageView/types.ts b/src/ui/components/MessageView/types.ts index 3d734f89..5339513b 100644 --- a/src/ui/components/MessageView/types.ts +++ b/src/ui/components/MessageView/types.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../../session-types"; +import type { SessionMessage } from "../../../session/types"; export type MessageViewProps = { message: SessionMessage; diff --git a/src/ui/components/MessageView/utils.ts b/src/ui/components/MessageView/utils.ts index 164b5fb4..9b004dbf 100644 --- a/src/ui/components/MessageView/utils.ts +++ b/src/ui/components/MessageView/utils.ts @@ -1,5 +1,5 @@ import type { DiffPreviewLine, ToolSummary } from "./types"; -import type { SessionMessage } from "../../../session-types"; +import type { SessionMessage } from "../../../session/types"; import { RawMode } from "../../contexts"; import chalk from "chalk"; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index db446040..12ec226a 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,8 +1,8 @@ import Index from "../DropdownMenu"; import React, { useEffect, useState } from "react"; -import type { SkillInfo } from "../../../session-types"; +import type { SkillInfo } from "../../../session/types"; import { useInput } from "ink"; -import { isSkillSelected } from "../../SlashCommandMenu"; +import { isSkillSelected } from "../../views/SlashCommandMenu"; const SkillsDropdown: React.FC<{ open: boolean; diff --git a/src/ui/core/askUserQuestion.ts b/src/ui/core/askUserQuestion.ts index ea94342e..8918604a 100644 --- a/src/ui/core/askUserQuestion.ts +++ b/src/ui/core/askUserQuestion.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../../session-types"; +import type { SessionMessage, SessionStatus } from "../../session/types"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/core/loadingText.ts b/src/ui/core/loadingText.ts index 738b9685..f74cc1ac 100644 --- a/src/ui/core/loadingText.ts +++ b/src/ui/core/loadingText.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../../session-types"; +import type { LlmStreamProgress, SessionEntry } from "../../session/types"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/core/slashCommands.ts b/src/ui/core/slashCommands.ts index d96734b0..8a7487b0 100644 --- a/src/ui/core/slashCommands.ts +++ b/src/ui/core/slashCommands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../../session-types"; +import type { SkillInfo } from "../../session/types"; export type SlashCommandKind = | "skill" diff --git a/src/ui/core/thinkingState.ts b/src/ui/core/thinkingState.ts index e7a525a7..bbd8e030 100644 --- a/src/ui/core/thinkingState.ts +++ b/src/ui/core/thinkingState.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../session-types"; +import type { SessionMessage } from "../../session/types"; /** * Returns the message id of the assistant "thinking" message that should stay diff --git a/src/ui/exitSummary.ts b/src/ui/exitSummary.ts index 95e11e7b..1801bd85 100644 --- a/src/ui/exitSummary.ts +++ b/src/ui/exitSummary.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import gradientString from "gradient-string"; -import type { ModelUsage, SessionEntry } from "../session-types"; +import type { ModelUsage, SessionEntry } from "../session/types"; type ExitSummaryInput = { session: SessionEntry | null; diff --git a/src/ui/hooks/index.ts b/src/ui/hooks/index.ts index 56e07251..86245b65 100644 --- a/src/ui/hooks/index.ts +++ b/src/ui/hooks/index.ts @@ -10,8 +10,8 @@ export { getPromptCursorPlacement, } from "./cursor"; -export { usePasteHandling } from "./paste-handling"; -export type { PasteRegion, PasteHandlingState, PasteHandlingActions } from "./paste-handling"; +export { usePasteHandling } from "./usePasteHandling"; +export type { PasteRegion, PasteHandlingState, PasteHandlingActions } from "./usePasteHandling"; -export { useHistoryNavigation } from "./history-navigation"; -export type { HistoryNavigationState, HistoryNavigationActions } from "./history-navigation"; +export { useHistoryNavigation } from "./useHistoryNavigation"; +export type { HistoryNavigationState, HistoryNavigationActions } from "./useHistoryNavigation"; diff --git a/src/ui/hooks/history-navigation.ts b/src/ui/hooks/useHistoryNavigation.ts similarity index 100% rename from src/ui/hooks/history-navigation.ts rename to src/ui/hooks/useHistoryNavigation.ts diff --git a/src/ui/hooks/paste-handling.ts b/src/ui/hooks/usePasteHandling.ts similarity index 100% rename from src/ui/hooks/paste-handling.ts rename to src/ui/hooks/usePasteHandling.ts diff --git a/src/ui/index.ts b/src/ui/index.ts index 482e59f9..d9077eee 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -7,8 +7,8 @@ import { export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; export { buildPromptDraftFromSessionMessage } from "./utils"; export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./hooks/cursor"; -export { default as AppContainer } from "./AppContainer"; -export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; +export { default as AppContainer } from "./views/AppContainer"; +export { AskUserQuestionPrompt } from "./views/AskUserQuestionPrompt"; export { MessageView } from "./components"; export { parseDiffPreview } from "./components/MessageView/utils"; export { @@ -23,17 +23,13 @@ export { getPromptReturnKeyAction, renderBufferWithCursor, buildInitPromptSubmission, - useTerminalInput, - parseTerminalInput, - dispatchTerminalInput, type PromptSubmission, type PromptDraft, - type InputKey, -} from "./PromptInput"; -export { SessionList, formatSessionTitle, filterSessions, formatSessionStatus } from "./SessionList"; -export { ThemedGradient } from "./ThemedGradient"; -export { UpdatePrompt, type UpdatePromptChoice } from "./UpdatePrompt"; -export { WelcomeScreen, formatHomeRelativePath, buildWelcomeTips } from "./WelcomeScreen"; +} from "./views/PromptInput"; +export { SessionList, formatSessionTitle, filterSessions, formatSessionStatus } from "./views/SessionList"; +export { ThemedGradient } from "./views/ThemedGradient"; +export { UpdatePrompt, type UpdatePromptChoice } from "./views/UpdatePrompt"; +export { WelcomeScreen, formatHomeRelativePath, buildWelcomeTips } from "./views/WelcomeScreen"; export { findPendingAskUserQuestion, formatAskUserQuestionAnswers, diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index 4a201466..4fb2fb1f 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -1,9 +1,9 @@ import chalk from "chalk"; import { renderMessageToStdout } from "../components/MessageView/utils"; import type { RawMode } from "../contexts"; -import type { PromptDraft } from "../PromptInput"; +import type { PromptDraft } from "../views/PromptInput"; import type { ModelConfigSelection } from "../../settings"; -import type { SessionEntry, SessionMessage } from "../../session-types"; +import type { SessionEntry, SessionMessage } from "../../session/types"; import type { SessionManager } from "../../session"; /** diff --git a/src/ui/App.tsx b/src/ui/views/App.tsx similarity index 97% rename from src/ui/App.tsx rename to src/ui/views/App.tsx index bea96f66..6ba5b623 100644 --- a/src/ui/App.tsx +++ b/src/ui/views/App.tsx @@ -1,15 +1,15 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; -import { createOpenAIClient } from "../common/openai-client"; -import type { PermissionScope } from "../settings"; -import { type ModelConfigSelection } from "../settings"; +import { createOpenAIClient } from "../../common/openai-client"; +import type { PermissionScope } from "../../settings"; +import { type ModelConfigSelection } from "../../settings"; import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptInput"; -import { MessageView, RawModeExitPrompt } from "./components"; +import { MessageView, RawModeExitPrompt } from "../components"; import { SessionList } from "./SessionList"; import { type UndoRestoreMode, UndoSelector } from "./UndoSelector"; -import { buildLoadingText } from "./core/loadingText"; -import { findExpandedThinkingId } from "./core/thinkingState"; +import { buildLoadingText } from "../core/loadingText"; +import { findExpandedThinkingId } from "../core/thinkingState"; import { WelcomeScreen } from "./WelcomeScreen"; import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; import { McpStatusList } from "./McpStatusList"; @@ -18,11 +18,11 @@ import { type AskUserQuestionAnswers, findPendingAskUserQuestion, formatAskUserQuestionAnswers, -} from "./core/askUserQuestion"; +} from "../core/askUserQuestion"; import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt"; -import { buildExitSummaryText } from "./exitSummary"; -import { RawMode, useRawModeContext } from "./contexts"; -import { renderMessageToStdout } from "./components/MessageView/utils"; +import { buildExitSummaryText } from "../exitSummary"; +import { RawMode, useRawModeContext } from "../contexts"; +import { renderMessageToStdout } from "../components/MessageView/utils"; import { buildPromptDraftFromSessionMessage, buildStatusLine, @@ -30,10 +30,10 @@ import { formatModelConfig, isCurrentSessionEmpty, renderRawModeMessages, -} from "./utils"; -import { resolveCurrentSettings, writeModelConfigSelection } from "../settings"; -import { isCollapsedThinking } from "./core/thinkingState"; -import { ANSI_CLEAR_SCREEN } from "./constants"; +} from "../utils"; +import { resolveCurrentSettings, writeModelConfigSelection } from "../../settings"; +import { isCollapsedThinking } from "../core/thinkingState"; +import { ANSI_CLEAR_SCREEN } from "../constants"; import type { LlmStreamProgress, MessageMeta, @@ -43,8 +43,8 @@ import type { SkillInfo, UndoTarget, UserPromptContent, -} from "../session-types"; -import { SessionManager } from "../session"; +} from "../../session/types"; +import { SessionManager } from "../../session"; type View = "chat" | "session-list" | "undo" | "mcp-status"; diff --git a/src/ui/AppContainer.tsx b/src/ui/views/AppContainer.tsx similarity index 85% rename from src/ui/AppContainer.tsx rename to src/ui/views/AppContainer.tsx index f36eb4aa..d5f6363a 100644 --- a/src/ui/AppContainer.tsx +++ b/src/ui/views/AppContainer.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { AppContext } from "./contexts"; +import { AppContext } from "../contexts"; import App from "./App"; -import { RawModeProvider } from "./contexts"; +import { RawModeProvider } from "../contexts"; const AppContainer: React.FC<{ projectRoot: string; diff --git a/src/ui/AskUserQuestionPrompt.tsx b/src/ui/views/AskUserQuestionPrompt.tsx similarity index 99% rename from src/ui/AskUserQuestionPrompt.tsx rename to src/ui/views/AskUserQuestionPrompt.tsx index 058de3e1..988215f9 100644 --- a/src/ui/AskUserQuestionPrompt.tsx +++ b/src/ui/views/AskUserQuestionPrompt.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import type { AskUserQuestionAnswers, AskUserQuestionItem } from "./core/askUserQuestion"; -import { useTerminalInput } from "./hooks"; +import type { AskUserQuestionAnswers, AskUserQuestionItem } from "../core/askUserQuestion"; +import { useTerminalInput } from "../hooks"; type Props = { questions: AskUserQuestionItem[]; diff --git a/src/ui/McpStatusList.tsx b/src/ui/views/McpStatusList.tsx similarity index 99% rename from src/ui/McpStatusList.tsx rename to src/ui/views/McpStatusList.tsx index 095612a2..4013ff81 100644 --- a/src/ui/McpStatusList.tsx +++ b/src/ui/views/McpStatusList.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { McpServerStatus } from "../mcp/mcp-manager"; +import type { McpServerStatus } from "../../mcp/mcp-manager"; type Props = { statuses: McpServerStatus[]; diff --git a/src/ui/PermissionPrompt.tsx b/src/ui/views/PermissionPrompt.tsx similarity index 98% rename from src/ui/PermissionPrompt.tsx rename to src/ui/views/PermissionPrompt.tsx index f450ac96..320dd7ab 100644 --- a/src/ui/PermissionPrompt.tsx +++ b/src/ui/views/PermissionPrompt.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import { useTerminalInput } from "./hooks"; -import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../common/permissions"; -import type { PermissionScope } from "../settings"; +import { useTerminalInput } from "../hooks"; +import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../../common/permissions"; +import type { PermissionScope } from "../../settings"; export type PermissionPromptResult = { permissions: UserToolPermission[]; diff --git a/src/ui/ProcessStdoutView.tsx b/src/ui/views/ProcessStdoutView.tsx similarity index 98% rename from src/ui/ProcessStdoutView.tsx rename to src/ui/views/ProcessStdoutView.tsx index 23b230aa..d43c39cb 100644 --- a/src/ui/ProcessStdoutView.tsx +++ b/src/ui/views/ProcessStdoutView.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; -import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../common/system/bash-timeout"; -import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../session-types"; -import { useTerminalInput } from "./hooks"; +import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/system/bash-timeout"; +import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session/types"; +import { useTerminalInput } from "../hooks"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/PromptInput.tsx b/src/ui/views/PromptInput.tsx similarity index 96% rename from src/ui/PromptInput.tsx rename to src/ui/views/PromptInput.tsx index 27af870b..824ec98e 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text, useApp, useStdout } from "ink"; import chalk from "chalk"; -import { ARGS_SEPARATOR } from "./constants"; +import { ARGS_SEPARATOR } from "../constants"; import { EMPTY_BUFFER, PASTE_MARKER_REGEX, @@ -24,45 +24,38 @@ import { moveWordLeft, moveWordRight, moveUp, -} from "./core/promptBuffer"; -import type { PromptBufferState } from "./core/promptBuffer"; +} from "../core/promptBuffer"; +import type { PromptBufferState } from "../core/promptBuffer"; import { clearPromptUndoRedoState, createPromptUndoRedoState, recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "./core/promptUndoRedo"; -import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "./core/slashCommands"; -import type { SlashCommandItem } from "./core/slashCommands"; +} from "../core/promptUndoRedo"; +import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "../core/slashCommands"; +import type { SlashCommandItem } from "../core/slashCommands"; import { filterFileMentionItems, getCurrentFileMentionToken, replaceCurrentFileMentionToken, scanFileMentionItems, -} from "./core/fileMentions"; -import type { FileMentionItem } from "./core/fileMentions"; -import { readClipboardImageAsync } from "./core/clipboard"; - -// Re-exported from prompt modules for backward compatibility -export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./hooks"; -export type { InputKey } from "./hooks"; - -import { useTerminalInput } from "./hooks"; -import type { InputKey } from "./hooks"; -import { usePasteHandling } from "./hooks/paste-handling"; -import { useHistoryNavigation } from "./hooks/history-navigation"; +} from "../core/fileMentions"; +import type { FileMentionItem } from "../core/fileMentions"; +import { readClipboardImageAsync } from "../core/clipboard"; +import { useTerminalInput, usePasteHandling, useHistoryNavigation } from "../hooks"; +import type { InputKey } from "../hooks"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, useBracketedPaste, useTerminalFocusReporting, -} from "./hooks"; +} from "../hooks"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; -import type { ModelConfigSelection, PermissionScope } from "../settings"; -import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; -import type { SessionEntry, SkillInfo } from "../session-types"; -import type { UserToolPermission } from "../common/permissions"; +import type { ModelConfigSelection, PermissionScope } from "../../settings"; +import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "../components"; +import type { SessionEntry, SkillInfo } from "../../session/types"; +import type { UserToolPermission } from "../../common/permissions"; export type PromptSubmission = { text: string; diff --git a/src/ui/SessionList.tsx b/src/ui/views/SessionList.tsx similarity index 98% rename from src/ui/SessionList.tsx rename to src/ui/views/SessionList.tsx index 4ea620e7..0b81ee89 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/views/SessionList.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { SessionEntry, SessionStatus } from "../session-types"; -import { truncate } from "./components/MessageView/utils"; +import type { SessionEntry, SessionStatus } from "../../session/types"; +import { truncate } from "../components/MessageView/utils"; type Props = { sessions: SessionEntry[]; diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/views/SlashCommandMenu.tsx similarity index 93% rename from src/ui/SlashCommandMenu.tsx rename to src/ui/views/SlashCommandMenu.tsx index 0ca5c696..275cf849 100644 --- a/src/ui/SlashCommandMenu.tsx +++ b/src/ui/views/SlashCommandMenu.tsx @@ -1,9 +1,9 @@ -import { formatSlashCommandDescription, formatSlashCommandLabel } from "./core/slashCommands"; -import type { SlashCommandItem } from "./core/slashCommands"; -import { ARGS_SEPARATOR } from "./constants"; +import { formatSlashCommandDescription, formatSlashCommandLabel } from "../core/slashCommands"; +import type { SlashCommandItem } from "../core/slashCommands"; +import { ARGS_SEPARATOR } from "../constants"; import React from "react"; import { Box, Text } from "ink"; -import type { SkillInfo } from "../session-types"; +import type { SkillInfo } from "../../session/types"; type SlashCommandMenuProps = { items: SlashCommandItem[]; diff --git a/src/ui/ThemedGradient.tsx b/src/ui/views/ThemedGradient.tsx similarity index 100% rename from src/ui/ThemedGradient.tsx rename to src/ui/views/ThemedGradient.tsx diff --git a/src/ui/UndoSelector.tsx b/src/ui/views/UndoSelector.tsx similarity index 99% rename from src/ui/UndoSelector.tsx rename to src/ui/views/UndoSelector.tsx index e41993e2..1d45acb0 100644 --- a/src/ui/UndoSelector.tsx +++ b/src/ui/views/UndoSelector.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { UndoTarget } from "../session-types"; +import type { UndoTarget } from "../../session/types"; export type UndoRestoreMode = "code-and-conversation" | "conversation"; diff --git a/src/ui/UpdatePrompt.tsx b/src/ui/views/UpdatePrompt.tsx similarity index 100% rename from src/ui/UpdatePrompt.tsx rename to src/ui/views/UpdatePrompt.tsx diff --git a/src/ui/WelcomeScreen.tsx b/src/ui/views/WelcomeScreen.tsx similarity index 94% rename from src/ui/WelcomeScreen.tsx rename to src/ui/views/WelcomeScreen.tsx index 36bb3030..9bbc8f1c 100644 --- a/src/ui/WelcomeScreen.tsx +++ b/src/ui/views/WelcomeScreen.tsx @@ -2,12 +2,12 @@ import React, { useMemo, useState } from "react"; import { Box, Text } from "ink"; import * as os from "node:os"; import path from "node:path"; -import type { SkillInfo } from "../session-types"; -import type { ResolvedDeepcodingSettings } from "../settings"; -import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./core/slashCommands"; +import type { SkillInfo } from "../../session/types"; +import type { ResolvedDeepcodingSettings } from "../../settings"; +import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slashCommands"; import { ThemedGradient } from "./ThemedGradient"; -import { AsciiLogo } from "./AsciiArt"; -import { useAppContext } from "./contexts"; +import { AsciiLogo } from "../AsciiArt"; +import { useAppContext } from "../contexts"; type WelcomeScreenProps = { projectRoot: string; From 0a1a40533883f4808cf5ca5fb291cd6e1149b0f2 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 20:56:43 +0800 Subject: [PATCH 082/212] =?UTF-8?q?style(ui):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E9=A2=9C=E8=89=B2=E5=92=8C=E6=96=87=E5=AD=97?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在App视图中取消撤销提示框的草稿状态 - 修改DropdownMenu标题颜色为#229ac3,增强视觉统一 - 精简McpStatusList的状态文本样式,去除加粗效果 - 将UndoSelector中的标题文字颜色改为#229ac3,提升界面一致性 --- src/ui/components/DropdownMenu/index.tsx | 2 +- src/ui/views/App.tsx | 1 + src/ui/views/McpStatusList.tsx | 18 ++++-------------- src/ui/views/UndoSelector.tsx | 2 +- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/ui/components/DropdownMenu/index.tsx b/src/ui/components/DropdownMenu/index.tsx index 6593ff8d..cf323141 100644 --- a/src/ui/components/DropdownMenu/index.tsx +++ b/src/ui/components/DropdownMenu/index.tsx @@ -64,7 +64,7 @@ const DropdownMenu = React.memo(function DropdownMenu({ maxVisible = 8, width, title, - titleColor = "magenta", + titleColor = "#229ac3", activeColor = "cyanBright", helpText, emptyText = "No items found", diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index 6ba5b623..dd30cb22 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -751,6 +751,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl targets={undoTargets} onSelect={(target, restoreMode) => void handleUndoRestore(target, restoreMode)} onCancel={() => { + setPromptDraft(null); setView("chat"); setShowWelcome(true); }} diff --git a/src/ui/views/McpStatusList.tsx b/src/ui/views/McpStatusList.tsx index 4013ff81..40d2f3f4 100644 --- a/src/ui/views/McpStatusList.tsx +++ b/src/ui/views/McpStatusList.tsx @@ -195,20 +195,10 @@ function ServerListView({
( - - {readyCount} ready, - - - {startingCount} starting, - - {reconnectingCount > 0 && ( - - {reconnectingCount} reconnecting, - - )} - - {failedCount} failed - + {readyCount} ready, + {startingCount} starting, + {reconnectingCount > 0 && {reconnectingCount} reconnecting,} + {failedCount} failed )
diff --git a/src/ui/views/UndoSelector.tsx b/src/ui/views/UndoSelector.tsx index 1d45acb0..613025c6 100644 --- a/src/ui/views/UndoSelector.tsx +++ b/src/ui/views/UndoSelector.tsx @@ -99,7 +99,7 @@ export function UndoSelector({ targets, onSelect, onCancel }: Props): React.Reac > - + Undo restore to the point before a prompt From 197676ec0687757d35d6940a54c31d3866e8d06c Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 21:52:30 +0800 Subject: [PATCH 083/212] =?UTF-8?q?refactor(session):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=B9=B6=E8=BF=81=E7=A7=BB=E4=BC=9A=E8=AF=9D=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将会话相关的工具函数从 session/index.ts 中移除 - 在 session/utils.ts 中重新实现并导出这些工具函数 - 调整了导入路径,改用新的工具函数模块 - 修正了多个文件中对 executeValidatedTool 的导入路径 - 统一了 SkillsDropdown 组件中的 DropdownMenu 导入名称 --- .../runtime/{runtime.ts => validate.ts} | 0 src/session/index.ts | 81 +++---------------- src/session/utils.ts | 70 +++++++++++++++- src/tools/edit-handler.ts | 2 +- src/tools/update-plan-handler.ts | 2 +- src/tools/write-handler.ts | 2 +- src/ui/components/SkillsDropdown/index.tsx | 4 +- 7 files changed, 83 insertions(+), 78 deletions(-) rename src/common/runtime/{runtime.ts => validate.ts} (100%) diff --git a/src/common/runtime/runtime.ts b/src/common/runtime/validate.ts similarity index 100% rename from src/common/runtime/runtime.ts rename to src/common/runtime/validate.ts diff --git a/src/session/index.ts b/src/session/index.ts index 704e7692..bab18b80 100644 --- a/src/session/index.ts +++ b/src/session/index.ts @@ -2,7 +2,6 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; -import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; @@ -44,7 +43,15 @@ import { type UserToolPermission, } from "../common/permissions"; -import { getCompactPromptTokenThreshold, getTotalTokens } from "./utils"; +import { + accumulateUsage, + accumulateUsagePerModel, + getCompactPromptTokenThreshold, + getExtensionRoot, + getTotalTokens, + isUsageRecord, + summarizeCompletionOptions, +} from "./utils"; import { type BashTimeoutAdjustment, type LlmStreamProgress, @@ -72,76 +79,6 @@ type ChatCompletionDebugOptions = { params?: Record; }; -function isUsageRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -function summarizeCompletionOptions(options?: Record): Record | undefined { - if (!options) { - return undefined; - } - return { - ...options, - signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, - }; -} - -function addUsageValue(current: unknown, next: unknown): unknown { - if (typeof next === "number") { - return (typeof current === "number" ? current : 0) + next; - } - - if (isUsageRecord(next)) { - const currentRecord = isUsageRecord(current) ? current : {}; - const result: Record = { ...currentRecord }; - for (const [key, value] of Object.entries(next)) { - result[key] = addUsageValue(currentRecord[key], value); - } - return result; - } - - return next; -} - -function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { - if (next == null) { - return current ?? null; - } - return addUsageValue(current, next) as ModelUsage; -} - -function usageWithRequestCount(usage: ModelUsage): ModelUsage { - const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; - return { - ...usage, - total_reqs: totalReqs, - }; -} - -function accumulateUsagePerModel( - current: Record | null | undefined, - model: string, - next: ModelUsage | null | undefined -): Record | null { - if (next == null) { - return current ?? null; - } - - const usagePerModel = { ...(current ?? {}) }; - const modelName = model.trim() || "unknown"; - usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; - return usagePerModel; -} - -function getExtensionRoot(): string { - if (typeof __dirname !== "undefined") { - return path.resolve(__dirname, "../.."); - } - - const currentFilePath = fileURLToPath(import.meta.url); - return path.resolve(path.dirname(currentFilePath), "../.."); -} - export class SessionManager { private readonly projectRoot: string; private readonly createOpenAIClient: CreateOpenAIClient; diff --git a/src/session/utils.ts b/src/session/utils.ts index 3b807002..50047cdb 100644 --- a/src/session/utils.ts +++ b/src/session/utils.ts @@ -1,3 +1,5 @@ +import * as path from "path"; +import { fileURLToPath } from "url"; import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; import type { ModelUsage } from "./types"; @@ -10,7 +12,7 @@ export function getCompactPromptTokenThreshold(model: string): number { : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; } -function isUsageRecord(value: unknown): value is Record { +export function isUsageRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -21,3 +23,69 @@ export function getTotalTokens(usage: ModelUsage | null | undefined): number { const totalTokens = (usage as Record).total_tokens; return typeof totalTokens === "number" ? totalTokens : 0; } + +export function summarizeCompletionOptions(options?: Record): Record | undefined { + if (!options) { + return undefined; + } + return { + ...options, + signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, + }; +} + +export function addUsageValue(current: unknown, next: unknown): unknown { + if (typeof next === "number") { + return (typeof current === "number" ? current : 0) + next; + } + + if (isUsageRecord(next)) { + const currentRecord = isUsageRecord(current) ? current : {}; + const result: Record = { ...currentRecord }; + for (const [key, value] of Object.entries(next)) { + result[key] = addUsageValue(currentRecord[key], value); + } + return result; + } + + return next; +} + +export function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { + if (next == null) { + return current ?? null; + } + return addUsageValue(current, next) as ModelUsage; +} + +export function usageWithRequestCount(usage: ModelUsage): ModelUsage { + const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; + return { + ...usage, + total_reqs: totalReqs, + }; +} + +export function accumulateUsagePerModel( + current: Record | null | undefined, + model: string, + next: ModelUsage | null | undefined +): Record | null { + if (next == null) { + return current ?? null; + } + + const usagePerModel = { ...(current ?? {}) }; + const modelName = model.trim() || "unknown"; + usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; + return usagePerModel; +} + +export function getExtensionRoot(): string { + if (typeof __dirname !== "undefined") { + return path.resolve(__dirname, "../.."); + } + + const currentFilePath = fileURLToPath(import.meta.url); + return path.resolve(path.dirname(currentFilePath), "../.."); +} diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index 98afa43f..6bf06112 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -8,7 +8,7 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool, semanticBoolean } from "../common/runtime/runtime"; +import { executeValidatedTool, semanticBoolean } from "../common/runtime/validate"; import { createSnippet, getFileState, diff --git a/src/tools/update-plan-handler.ts b/src/tools/update-plan-handler.ts index a8947cfc..ff848703 100644 --- a/src/tools/update-plan-handler.ts +++ b/src/tools/update-plan-handler.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { ToolExecutionContext, ToolExecutionResult } from "./executor"; -import { executeValidatedTool } from "../common/runtime/runtime"; +import { executeValidatedTool } from "../common/runtime/validate"; const updatePlanSchema = z.strictObject({ plan: z.string().trim().min(1, "plan must not be empty."), diff --git a/src/tools/write-handler.ts b/src/tools/write-handler.ts index e91a78c7..1d3fb558 100644 --- a/src/tools/write-handler.ts +++ b/src/tools/write-handler.ts @@ -9,7 +9,7 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool } from "../common/runtime/runtime"; +import { executeValidatedTool } from "../common/runtime/validate"; import { getFileState, isAbsoluteFilePath, diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index 12ec226a..07b49de6 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,4 +1,4 @@ -import Index from "../DropdownMenu"; +import DropdownMenu from "../DropdownMenu"; import React, { useEffect, useState } from "react"; import type { SkillInfo } from "../../../session/types"; import { useInput } from "ink"; @@ -52,7 +52,7 @@ const SkillsDropdown: React.FC<{ } return ( - Date: Tue, 26 May 2026 09:32:44 +0800 Subject: [PATCH 084/212] =?UTF-8?q?refactor(session):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=20getExtensionRoot=20=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 getExtensionRoot 函数从 session/utils.ts 移除 - 在 session/utils.ts 中重新导出 prompt.ts 中的 getExtensionRoot 函数 - 调整 prompt.ts 中 getExtensionRoot 函数的导出为 export - 优化了 getExtensionRoot 函数的注释和环境兼容处理逻辑 --- src/prompt.ts | 4 ++-- src/session/utils.ts | 11 +---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/prompt.ts b/src/prompt.ts index f4e76d9d..e77993fc 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -2,8 +2,8 @@ import { execFileSync, execSync } from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { fileURLToPath } from "url"; import ejs from "ejs"; +import { fileURLToPath } from "url"; import type { SessionMessage } from "./session/types"; import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; @@ -286,7 +286,7 @@ function getUnameInfo(): string { } } -function getExtensionRoot(): string { +export function getExtensionRoot(): string { // Prefer `__dirname` which is always available in the CJS bundle output. // Fall back to `import.meta.url` for ESM test environments (tsx --test). if (typeof __dirname !== "undefined") { diff --git a/src/session/utils.ts b/src/session/utils.ts index 50047cdb..552e7725 100644 --- a/src/session/utils.ts +++ b/src/session/utils.ts @@ -1,5 +1,3 @@ -import * as path from "path"; -import { fileURLToPath } from "url"; import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; import type { ModelUsage } from "./types"; @@ -81,11 +79,4 @@ export function accumulateUsagePerModel( return usagePerModel; } -export function getExtensionRoot(): string { - if (typeof __dirname !== "undefined") { - return path.resolve(__dirname, "../.."); - } - - const currentFilePath = fileURLToPath(import.meta.url); - return path.resolve(path.dirname(currentFilePath), "../.."); -} +export { getExtensionRoot } from "../prompt"; From 77245d8eaab3b0eff1bba718682047d844bf253c Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 10:06:52 +0800 Subject: [PATCH 085/212] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E6=AD=A3=E6=9D=83?= =?UTF-8?q?=E9=99=90=E8=AF=B7=E6=B1=82=E6=B5=81=E7=A8=8B=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=8D=89=E7=A8=BF=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在权限请求未通过时清理提示草稿,避免残留内容 - 当权限被拒绝时不再重复清空提示草稿 - 取消欢迎界面的重复显示,优化视图切换逻辑 --- src/ui/views/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index dd30cb22..4f614c5d 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -659,6 +659,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl if (!sessionId) { return; } + setPromptDraft(null); if (result.hasDeny) { setPendingPermissionReply({ sessionId, @@ -666,7 +667,6 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl alwaysAllows: result.alwaysAllows, }); setStatusLine("Permission denied. Add a reply, then press Enter to continue."); - setPromptDraft(null); sessionManager.denySessionPermission(sessionId); return; } @@ -753,7 +753,6 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl onCancel={() => { setPromptDraft(null); setView("chat"); - setShowWelcome(true); }} /> ) : view === "mcp-status" ? ( From 39b38f31c7281e0326ba7fc3c6e919e29865cecc Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 10:57:03 +0800 Subject: [PATCH 086/212] =?UTF-8?q?refactor(core):=20=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E5=A4=9A=E4=B8=AA=E6=96=87=E4=BB=B6=E5=8F=8A=E5=85=B6?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=B7=AF=E5=BE=84=E4=BB=A5=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将核心模块文件名改为短横线风格(kebab-case) - 更新所有相关导入路径以匹配重命名后的文件名 - 修改测试文件名和对应引用路径,保持一致性 - 调整部分组件及钩子中的类型导入路径 - 无业务功能变更,仅代码结构及命名优化 --- src/cli.tsx | 2 +- src/common/{updateCheck.ts => update-check.ts} | 0 ...rQuestion.test.ts => ask-user-question.test.ts} | 0 ...{dropdownMenu.test.ts => dropdown-menu.test.ts} | 0 .../{exitSummary.test.ts => exit-summary.test.ts} | 0 ...{fileMentions.test.ts => file-mentions.test.ts} | 2 +- .../{loadingText.test.ts => loading-text.test.ts} | 0 .../{messageView.test.ts => message-view.test.ts} | 0 ...{promptBuffer.test.ts => prompt-buffer.test.ts} | 0 ...InputKeys.test.ts => prompt-input-keys.test.ts} | 0 ...ptUndoRedo.test.ts => prompt-undo-redo.test.ts} | 2 +- .../{sessionList.test.ts => session-list.test.ts} | 0 ...lashCommands.test.ts => slash-commands.test.ts} | 0 ...hinkingState.test.ts => thinking-state.test.ts} | 0 .../{updateCheck.test.ts => update-check.test.ts} | 2 +- ...elcomeScreen.test.ts => welcome-screen.test.ts} | 0 src/ui/{AsciiArt.ts => ascii-art.ts} | 0 src/ui/components/FileMentionMenu/index.tsx | 2 +- .../{askUserQuestion.ts => ask-user-question.ts} | 0 src/ui/core/{fileMentions.ts => file-mentions.ts} | 2 +- src/ui/core/{loadingText.ts => loading-text.ts} | 0 src/ui/core/{promptBuffer.ts => prompt-buffer.ts} | 0 .../{promptUndoRedo.ts => prompt-undo-redo.ts} | 2 +- .../core/{slashCommands.ts => slash-commands.ts} | 0 .../core/{thinkingState.ts => thinking-state.ts} | 0 src/ui/{exitSummary.ts => exit-summary.ts} | 0 src/ui/hooks/cursor.ts | 2 +- src/ui/hooks/useHistoryNavigation.ts | 2 +- src/ui/hooks/usePasteHandling.ts | 4 ++-- src/ui/index.ts | 14 +++++++------- src/ui/views/App.tsx | 10 +++++----- src/ui/views/AskUserQuestionPrompt.tsx | 2 +- src/ui/views/PromptInput.tsx | 14 +++++++------- src/ui/views/SlashCommandMenu.tsx | 4 ++-- src/ui/views/WelcomeScreen.tsx | 4 ++-- 35 files changed, 35 insertions(+), 35 deletions(-) rename src/common/{updateCheck.ts => update-check.ts} (100%) rename src/tests/{askUserQuestion.test.ts => ask-user-question.test.ts} (100%) rename src/tests/{dropdownMenu.test.ts => dropdown-menu.test.ts} (100%) rename src/tests/{exitSummary.test.ts => exit-summary.test.ts} (100%) rename src/tests/{fileMentions.test.ts => file-mentions.test.ts} (99%) rename src/tests/{loadingText.test.ts => loading-text.test.ts} (100%) rename src/tests/{messageView.test.ts => message-view.test.ts} (100%) rename src/tests/{promptBuffer.test.ts => prompt-buffer.test.ts} (100%) rename src/tests/{promptInputKeys.test.ts => prompt-input-keys.test.ts} (100%) rename src/tests/{promptUndoRedo.test.ts => prompt-undo-redo.test.ts} (98%) rename src/tests/{sessionList.test.ts => session-list.test.ts} (100%) rename src/tests/{slashCommands.test.ts => slash-commands.test.ts} (100%) rename src/tests/{thinkingState.test.ts => thinking-state.test.ts} (100%) rename src/tests/{updateCheck.test.ts => update-check.test.ts} (97%) rename src/tests/{welcomeScreen.test.ts => welcome-screen.test.ts} (100%) rename src/ui/{AsciiArt.ts => ascii-art.ts} (100%) rename src/ui/core/{askUserQuestion.ts => ask-user-question.ts} (100%) rename src/ui/core/{fileMentions.ts => file-mentions.ts} (99%) rename src/ui/core/{loadingText.ts => loading-text.ts} (100%) rename src/ui/core/{promptBuffer.ts => prompt-buffer.ts} (100%) rename src/ui/core/{promptUndoRedo.ts => prompt-undo-redo.ts} (95%) rename src/ui/core/{slashCommands.ts => slash-commands.ts} (100%) rename src/ui/core/{thinkingState.ts => thinking-state.ts} (100%) rename src/ui/{exitSummary.ts => exit-summary.ts} (100%) diff --git a/src/cli.tsx b/src/cli.tsx index de26bf2c..d851a911 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "ink"; import { setShellIfWindows } from "./common/system/shell-utils"; -import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/updateCheck"; +import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; const args = process.argv.slice(2); diff --git a/src/common/updateCheck.ts b/src/common/update-check.ts similarity index 100% rename from src/common/updateCheck.ts rename to src/common/update-check.ts diff --git a/src/tests/askUserQuestion.test.ts b/src/tests/ask-user-question.test.ts similarity index 100% rename from src/tests/askUserQuestion.test.ts rename to src/tests/ask-user-question.test.ts diff --git a/src/tests/dropdownMenu.test.ts b/src/tests/dropdown-menu.test.ts similarity index 100% rename from src/tests/dropdownMenu.test.ts rename to src/tests/dropdown-menu.test.ts diff --git a/src/tests/exitSummary.test.ts b/src/tests/exit-summary.test.ts similarity index 100% rename from src/tests/exitSummary.test.ts rename to src/tests/exit-summary.test.ts diff --git a/src/tests/fileMentions.test.ts b/src/tests/file-mentions.test.ts similarity index 99% rename from src/tests/fileMentions.test.ts rename to src/tests/file-mentions.test.ts index 50a6dc41..93d9bccc 100644 --- a/src/tests/fileMentions.test.ts +++ b/src/tests/file-mentions.test.ts @@ -10,7 +10,7 @@ import { replaceCurrentFileMentionToken, scanFileMentionItems, type FileMentionItem, -} from "../ui/core/fileMentions"; +} from "../ui/core/file-mentions"; test("getCurrentFileMentionToken detects bare @file tokens under the cursor", () => { assert.deepEqual(getCurrentFileMentionToken({ text: "review @src/app.ts please", cursor: 10 }), { diff --git a/src/tests/loadingText.test.ts b/src/tests/loading-text.test.ts similarity index 100% rename from src/tests/loadingText.test.ts rename to src/tests/loading-text.test.ts diff --git a/src/tests/messageView.test.ts b/src/tests/message-view.test.ts similarity index 100% rename from src/tests/messageView.test.ts rename to src/tests/message-view.test.ts diff --git a/src/tests/promptBuffer.test.ts b/src/tests/prompt-buffer.test.ts similarity index 100% rename from src/tests/promptBuffer.test.ts rename to src/tests/prompt-buffer.test.ts diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/prompt-input-keys.test.ts similarity index 100% rename from src/tests/promptInputKeys.test.ts rename to src/tests/prompt-input-keys.test.ts diff --git a/src/tests/promptUndoRedo.test.ts b/src/tests/prompt-undo-redo.test.ts similarity index 98% rename from src/tests/promptUndoRedo.test.ts rename to src/tests/prompt-undo-redo.test.ts index 26360c04..d4590fb6 100644 --- a/src/tests/promptUndoRedo.test.ts +++ b/src/tests/prompt-undo-redo.test.ts @@ -8,7 +8,7 @@ import { recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "../ui/core/promptUndoRedo"; +} from "../ui/core/prompt-undo-redo"; test("prompt undo and redo restore edited buffer states", () => { const history = createPromptUndoRedoState(); diff --git a/src/tests/sessionList.test.ts b/src/tests/session-list.test.ts similarity index 100% rename from src/tests/sessionList.test.ts rename to src/tests/session-list.test.ts diff --git a/src/tests/slashCommands.test.ts b/src/tests/slash-commands.test.ts similarity index 100% rename from src/tests/slashCommands.test.ts rename to src/tests/slash-commands.test.ts diff --git a/src/tests/thinkingState.test.ts b/src/tests/thinking-state.test.ts similarity index 100% rename from src/tests/thinkingState.test.ts rename to src/tests/thinking-state.test.ts diff --git a/src/tests/updateCheck.test.ts b/src/tests/update-check.test.ts similarity index 97% rename from src/tests/updateCheck.test.ts rename to src/tests/update-check.test.ts index 23682de2..93b30360 100644 --- a/src/tests/updateCheck.test.ts +++ b/src/tests/update-check.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { compareVersions, parseNpmViewVersion } from "../common/updateCheck"; +import { compareVersions, parseNpmViewVersion } from "../common/update-check"; test("compareVersions orders semantic versions", () => { assert.equal(compareVersions("0.1.4", "0.1.3"), 1); diff --git a/src/tests/welcomeScreen.test.ts b/src/tests/welcome-screen.test.ts similarity index 100% rename from src/tests/welcomeScreen.test.ts rename to src/tests/welcome-screen.test.ts diff --git a/src/ui/AsciiArt.ts b/src/ui/ascii-art.ts similarity index 100% rename from src/ui/AsciiArt.ts rename to src/ui/ascii-art.ts diff --git a/src/ui/components/FileMentionMenu/index.tsx b/src/ui/components/FileMentionMenu/index.tsx index 15465d42..f00b367e 100644 --- a/src/ui/components/FileMentionMenu/index.tsx +++ b/src/ui/components/FileMentionMenu/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Box, Text } from "ink"; import { useInput } from "ink"; import DropdownMenu from "../DropdownMenu"; -import type { FileMentionItem, FileMentionToken } from "../../core/fileMentions"; +import type { FileMentionItem, FileMentionToken } from "../../core/file-mentions"; type Props = { open: boolean; diff --git a/src/ui/core/askUserQuestion.ts b/src/ui/core/ask-user-question.ts similarity index 100% rename from src/ui/core/askUserQuestion.ts rename to src/ui/core/ask-user-question.ts diff --git a/src/ui/core/fileMentions.ts b/src/ui/core/file-mentions.ts similarity index 99% rename from src/ui/core/fileMentions.ts rename to src/ui/core/file-mentions.ts index cbacbe6d..ae9c8b99 100644 --- a/src/ui/core/fileMentions.ts +++ b/src/ui/core/file-mentions.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import ignore from "ignore"; -import type { PromptBufferState } from "./promptBuffer"; +import type { PromptBufferState } from "./prompt-buffer"; export type FileMentionItem = { path: string; diff --git a/src/ui/core/loadingText.ts b/src/ui/core/loading-text.ts similarity index 100% rename from src/ui/core/loadingText.ts rename to src/ui/core/loading-text.ts diff --git a/src/ui/core/promptBuffer.ts b/src/ui/core/prompt-buffer.ts similarity index 100% rename from src/ui/core/promptBuffer.ts rename to src/ui/core/prompt-buffer.ts diff --git a/src/ui/core/promptUndoRedo.ts b/src/ui/core/prompt-undo-redo.ts similarity index 95% rename from src/ui/core/promptUndoRedo.ts rename to src/ui/core/prompt-undo-redo.ts index 9d30f57b..fd2870a6 100644 --- a/src/ui/core/promptUndoRedo.ts +++ b/src/ui/core/prompt-undo-redo.ts @@ -1,4 +1,4 @@ -import type { PromptBufferState } from "./promptBuffer"; +import type { PromptBufferState } from "./prompt-buffer"; export type PromptUndoRedoState = { undoStack: PromptBufferState[]; diff --git a/src/ui/core/slashCommands.ts b/src/ui/core/slash-commands.ts similarity index 100% rename from src/ui/core/slashCommands.ts rename to src/ui/core/slash-commands.ts diff --git a/src/ui/core/thinkingState.ts b/src/ui/core/thinking-state.ts similarity index 100% rename from src/ui/core/thinkingState.ts rename to src/ui/core/thinking-state.ts diff --git a/src/ui/exitSummary.ts b/src/ui/exit-summary.ts similarity index 100% rename from src/ui/exitSummary.ts rename to src/ui/exit-summary.ts diff --git a/src/ui/hooks/cursor.ts b/src/ui/hooks/cursor.ts index 2ecbddd7..07cc5779 100644 --- a/src/ui/hooks/cursor.ts +++ b/src/ui/hooks/cursor.ts @@ -1,5 +1,5 @@ import { useLayoutEffect, useRef } from "react"; -import type { PromptBufferState } from "../core/promptBuffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; type CursorPlacement = { rowsUp: number; diff --git a/src/ui/hooks/useHistoryNavigation.ts b/src/ui/hooks/useHistoryNavigation.ts index 1f595a9c..433d493c 100644 --- a/src/ui/hooks/useHistoryNavigation.ts +++ b/src/ui/hooks/useHistoryNavigation.ts @@ -1,6 +1,6 @@ import type React from "react"; import { useCallback, useState } from "react"; -import type { PromptBufferState } from "../core/promptBuffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; export type HistoryNavigationState = { historyCursor: number; diff --git a/src/ui/hooks/usePasteHandling.ts b/src/ui/hooks/usePasteHandling.ts index 1ecdd3d1..50cae754 100644 --- a/src/ui/hooks/usePasteHandling.ts +++ b/src/ui/hooks/usePasteHandling.ts @@ -1,7 +1,7 @@ import type React from "react"; import { useRef, useState } from "react"; -import type { PromptBufferState } from "../core/promptBuffer"; -import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../core/promptBuffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; +import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../core/prompt-buffer"; export type PasteRegion = { start: number; diff --git a/src/ui/index.ts b/src/ui/index.ts index d9077eee..ae1109ad 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -38,9 +38,9 @@ export { type AskUserQuestionItem, type PendingAskUserQuestion, type AskUserQuestionAnswers, -} from "./core/askUserQuestion"; +} from "./core/ask-user-question"; export { readClipboardImage, type ClipboardImage } from "./core/clipboard"; -export { buildLoadingText, type LoadingTextInput } from "./core/loadingText"; +export { buildLoadingText, type LoadingTextInput } from "./core/loading-text"; export { renderMarkdown, renderMarkdownSegments, type MarkdownSegment } from "./components/MessageView/markdown"; export { EMPTY_BUFFER, @@ -62,7 +62,7 @@ export { isEmpty, getCurrentSlashToken, type PromptBufferState, -} from "./core/promptBuffer"; +} from "./core/prompt-buffer"; export { BUILTIN_SLASH_COMMANDS, buildSlashCommands, @@ -72,7 +72,7 @@ export { formatSlashCommandLabel, type SlashCommandKind, type SlashCommandItem, -} from "./core/slashCommands"; +} from "./core/slash-commands"; export { filterFileMentionItems, formatFileMentionPath, @@ -81,6 +81,6 @@ export { scanFileMentionItems, type FileMentionItem, type FileMentionToken, -} from "./core/fileMentions"; -export { findExpandedThinkingId, isCollapsedThinking } from "./core/thinkingState"; -export { buildExitSummaryText } from "./exitSummary"; +} from "./core/file-mentions"; +export { findExpandedThinkingId, isCollapsedThinking } from "./core/thinking-state"; +export { buildExitSummaryText } from "./exit-summary"; diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index 4f614c5d..e8c41537 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -8,8 +8,8 @@ import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptIn import { MessageView, RawModeExitPrompt } from "../components"; import { SessionList } from "./SessionList"; import { type UndoRestoreMode, UndoSelector } from "./UndoSelector"; -import { buildLoadingText } from "../core/loadingText"; -import { findExpandedThinkingId } from "../core/thinkingState"; +import { buildLoadingText } from "../core/loading-text"; +import { findExpandedThinkingId } from "../core/thinking-state"; import { WelcomeScreen } from "./WelcomeScreen"; import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; import { McpStatusList } from "./McpStatusList"; @@ -18,9 +18,9 @@ import { type AskUserQuestionAnswers, findPendingAskUserQuestion, formatAskUserQuestionAnswers, -} from "../core/askUserQuestion"; +} from "../core/ask-user-question"; import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt"; -import { buildExitSummaryText } from "../exitSummary"; +import { buildExitSummaryText } from "../exit-summary"; import { RawMode, useRawModeContext } from "../contexts"; import { renderMessageToStdout } from "../components/MessageView/utils"; import { @@ -32,7 +32,7 @@ import { renderRawModeMessages, } from "../utils"; import { resolveCurrentSettings, writeModelConfigSelection } from "../../settings"; -import { isCollapsedThinking } from "../core/thinkingState"; +import { isCollapsedThinking } from "../core/thinking-state"; import { ANSI_CLEAR_SCREEN } from "../constants"; import type { LlmStreamProgress, diff --git a/src/ui/views/AskUserQuestionPrompt.tsx b/src/ui/views/AskUserQuestionPrompt.tsx index 988215f9..a2f91adb 100644 --- a/src/ui/views/AskUserQuestionPrompt.tsx +++ b/src/ui/views/AskUserQuestionPrompt.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import type { AskUserQuestionAnswers, AskUserQuestionItem } from "../core/askUserQuestion"; +import type { AskUserQuestionAnswers, AskUserQuestionItem } from "../core/ask-user-question"; import { useTerminalInput } from "../hooks"; type Props = { diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index 824ec98e..9067e71c 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -24,24 +24,24 @@ import { moveWordLeft, moveWordRight, moveUp, -} from "../core/promptBuffer"; -import type { PromptBufferState } from "../core/promptBuffer"; +} from "../core/prompt-buffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; import { clearPromptUndoRedoState, createPromptUndoRedoState, recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "../core/promptUndoRedo"; -import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "../core/slashCommands"; -import type { SlashCommandItem } from "../core/slashCommands"; +} from "../core/prompt-undo-redo"; +import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "../core/slash-commands"; +import type { SlashCommandItem } from "../core/slash-commands"; import { filterFileMentionItems, getCurrentFileMentionToken, replaceCurrentFileMentionToken, scanFileMentionItems, -} from "../core/fileMentions"; -import type { FileMentionItem } from "../core/fileMentions"; +} from "../core/file-mentions"; +import type { FileMentionItem } from "../core/file-mentions"; import { readClipboardImageAsync } from "../core/clipboard"; import { useTerminalInput, usePasteHandling, useHistoryNavigation } from "../hooks"; import type { InputKey } from "../hooks"; diff --git a/src/ui/views/SlashCommandMenu.tsx b/src/ui/views/SlashCommandMenu.tsx index 275cf849..5b6eb762 100644 --- a/src/ui/views/SlashCommandMenu.tsx +++ b/src/ui/views/SlashCommandMenu.tsx @@ -1,5 +1,5 @@ -import { formatSlashCommandDescription, formatSlashCommandLabel } from "../core/slashCommands"; -import type { SlashCommandItem } from "../core/slashCommands"; +import { formatSlashCommandDescription, formatSlashCommandLabel } from "../core/slash-commands"; +import type { SlashCommandItem } from "../core/slash-commands"; import { ARGS_SEPARATOR } from "../constants"; import React from "react"; import { Box, Text } from "ink"; diff --git a/src/ui/views/WelcomeScreen.tsx b/src/ui/views/WelcomeScreen.tsx index 9bbc8f1c..7cae2889 100644 --- a/src/ui/views/WelcomeScreen.tsx +++ b/src/ui/views/WelcomeScreen.tsx @@ -4,9 +4,9 @@ import * as os from "node:os"; import path from "node:path"; import type { SkillInfo } from "../../session/types"; import type { ResolvedDeepcodingSettings } from "../../settings"; -import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slashCommands"; +import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slash-commands"; import { ThemedGradient } from "./ThemedGradient"; -import { AsciiLogo } from "../AsciiArt"; +import { AsciiLogo } from "../ascii-art"; import { useAppContext } from "../contexts"; type WelcomeScreenProps = { From 192d02df326a8fa3b69c3e87406ceb82f761090d Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 11:27:22 +0800 Subject: [PATCH 087/212] =?UTF-8?q?refactor(session):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=20session=20=E6=96=87=E4=BB=B6=E5=A4=B9=E5=8F=8A=E4=B8=8B?= =?UTF-8?q?=E9=9D=A2=E7=9A=84=E5=85=A8=E9=83=A8=E6=96=87=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=20=E5=9B=9E=E6=BB=9A=E4=B8=BA=E7=BB=9F=E4=B8=80=E5=BC=95?= =?UTF-8?q?=E7=94=A8=20session=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了整个 session 文件夹及相关类型定义 - 统一所有模块中对 session 相关类型的导入,改为从 session.ts 模块直接导入 --- src/prompt.ts | 2 +- src/{session/index.ts => session.ts} | 276 ++++++++++++++++++--- src/session/types.ts | 140 ----------- src/session/utils.ts | 82 ------ src/tests/ask-user-question.test.ts | 2 +- src/tests/exit-summary.test.ts | 2 +- src/tests/message-view.test.ts | 2 +- src/tests/prompt-input-keys.test.ts | 2 +- src/tests/session-list.test.ts | 2 +- src/tests/session.test.ts | 2 +- src/tests/slash-commands.test.ts | 2 +- src/tests/thinking-state.test.ts | 2 +- src/ui/components/MessageView/types.ts | 2 +- src/ui/components/MessageView/utils.ts | 2 +- src/ui/components/SkillsDropdown/index.tsx | 2 +- src/ui/core/ask-user-question.ts | 2 +- src/ui/core/loading-text.ts | 2 +- src/ui/core/slash-commands.ts | 2 +- src/ui/core/thinking-state.ts | 2 +- src/ui/exit-summary.ts | 2 +- src/ui/utils/index.ts | 2 +- src/ui/views/App.tsx | 2 +- src/ui/views/ProcessStdoutView.tsx | 2 +- src/ui/views/PromptInput.tsx | 2 +- src/ui/views/SessionList.tsx | 2 +- src/ui/views/SlashCommandMenu.tsx | 2 +- src/ui/views/UndoSelector.tsx | 2 +- src/ui/views/WelcomeScreen.tsx | 2 +- 28 files changed, 260 insertions(+), 288 deletions(-) rename src/{session/index.ts => session.ts} (92%) delete mode 100644 src/session/types.ts delete mode 100644 src/session/utils.ts diff --git a/src/prompt.ts b/src/prompt.ts index e77993fc..9daea588 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -4,7 +4,7 @@ import * as os from "os"; import * as path from "path"; import ejs from "ejs"; import { fileURLToPath } from "url"; -import type { SessionMessage } from "./session/types"; +import type { SessionMessage } from "./session"; import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; diff --git a/src/session/index.ts b/src/session.ts similarity index 92% rename from src/session/index.ts rename to src/session.ts index bab18b80..c9e11b7d 100644 --- a/src/session/index.ts +++ b/src/session.ts @@ -1,3 +1,224 @@ +import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; +import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; +import type { CreateOpenAIClient } from "./tools/executor"; + +export type SessionStatus = + | "failed" + | "pending" + | "processing" + | "waiting_for_user" + | "completed" + | "interrupted" + | "ask_permission" + | "permission_denied"; + +export type ModelUsage = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + completion_tokens_details?: Record; + prompt_tokens_details?: Record; + prompt_cache_hit_tokens?: number; + prompt_cache_miss_tokens?: number; + total_reqs?: number; +}; + +export type SessionProcessEntry = { + startTime: string; + command: string; + timeoutMs?: number; + deadlineAt?: string; + timedOut?: boolean; +}; + +export type BashTimeoutAdjustment = { + processId: string; + timeoutMs: number; + deadlineAt: string; + timedOut: boolean; +}; + +export type SessionEntry = { + id: string; + summary: string | null; + assistantReply: string | null; + assistantThinking: string | null; + assistantRefusal: string | null; + toolCalls: unknown[] | null; + status: SessionStatus; + failReason: string | null; + usage: ModelUsage | null; + usagePerModel: Record | null; + activeTokens: number; + createTime: string; + updateTime: string; + processes: Map | null; + askPermissions?: AskPermissionRequest[]; +}; + +export type SessionsIndex = { + version: 1; + entries: SessionEntry[]; + originalPath: string; +}; + +export type SessionMessageRole = "system" | "user" | "assistant" | "tool"; + +export type MessageMeta = { + function?: unknown; + paramsMd?: string; + resultMd?: string; + asThinking?: boolean; + isSummary?: boolean; + isModelChange?: boolean; + skill?: SkillInfo; + permissions?: MessageToolPermission[]; + userPrompt?: UserPromptContent; +}; + +export type SessionMessage = { + id: string; + sessionId: string; + role: SessionMessageRole; + content: string | null; + contentParams: unknown | null; + messageParams: unknown | null; + compacted: boolean; + visible: boolean; + createTime: string; + updateTime: string; + meta?: MessageMeta; + html?: string; + checkpointHash?: string; +}; + +export type UndoTarget = { + message: SessionMessage; + index: number; + canRestoreCode: boolean; +}; + +export type UserPromptContent = { + text?: string; + imageUrls?: string[]; + skills?: SkillInfo[]; + permissions?: UserToolPermission[]; + alwaysAllows?: PermissionScope[]; +}; + +export type SkillInfo = { + name: string; + path: string; + description: string; + isLoaded?: boolean; +}; + +export type SessionManagerOptions = { + projectRoot: string; + createOpenAIClient: CreateOpenAIClient; + getResolvedSettings: () => { + model: string; + webSearchTool?: string; + mcpServers?: Record; + permissions?: Required; + }; + renderMarkdown: (text: string) => string; + onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; + onSessionEntryUpdated?: (entry: SessionEntry) => void; + onLlmStreamProgress?: (progress: LlmStreamProgress) => void; + onMcpStatusChanged?: () => void; + onProcessStdout?: (pid: number, chunk: string) => void; +}; + +export type LlmStreamProgress = { + requestId: string; + sessionId?: string; + startedAt: string; + estimatedTokens: number; + formattedTokens: string; + phase: "start" | "update" | "end"; +}; +import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; + +const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; +const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; + +export function getCompactPromptTokenThreshold(model: string): number { + return DEEPSEEK_V4_MODELS.has(model) + ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD + : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; +} + +export function isUsageRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function getTotalTokens(usage: ModelUsage | null | undefined): number { + if (!isUsageRecord(usage)) { + return 0; + } + const totalTokens = (usage as Record).total_tokens; + return typeof totalTokens === "number" ? totalTokens : 0; +} + +export function summarizeCompletionOptions(options?: Record): Record | undefined { + if (!options) { + return undefined; + } + return { + ...options, + signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, + }; +} + +export function addUsageValue(current: unknown, next: unknown): unknown { + if (typeof next === "number") { + return (typeof current === "number" ? current : 0) + next; + } + + if (isUsageRecord(next)) { + const currentRecord = isUsageRecord(current) ? current : {}; + const result: Record = { ...currentRecord }; + for (const [key, value] of Object.entries(next)) { + result[key] = addUsageValue(currentRecord[key], value); + } + return result; + } + + return next; +} + +export function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { + if (next == null) { + return current ?? null; + } + return addUsageValue(current, next) as ModelUsage; +} + +export function usageWithRequestCount(usage: ModelUsage): ModelUsage { + const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; + return { + ...usage, + total_reqs: totalReqs, + }; +} + +export function accumulateUsagePerModel( + current: Record | null | undefined, + model: string, + next: ModelUsage | null | undefined +): Record | null { + if (next == null) { + return current ?? null; + } + + const usagePerModel = { ...(current ?? {}) }; + const modelName = model.trim() || "unknown"; + usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; + return usagePerModel; +} + +export { getExtensionRoot } from "./prompt"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; @@ -5,68 +226,41 @@ import * as crypto from "crypto"; import matter from "gray-matter"; import ejs from "ejs"; import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; -import { launchNotifyScript } from "../common/notify"; -import { buildThinkingRequestOptions } from "../common/openai-thinking"; -import { supportsMultimodal } from "../common/model-capabilities"; +import { launchNotifyScript } from "./common/notify"; +import { buildThinkingRequestOptions } from "./common/openai-thinking"; +import { supportsMultimodal } from "./common/model-capabilities"; import { getCompactPrompt, getDefaultSkillPrompt, + getExtensionRoot, getRuntimeContext, getSystemPrompt, getTools, type ToolDefinition, -} from "../prompt"; +} from "./prompt"; import { - type CreateOpenAIClient, type ProcessTimeoutControl, type ProcessTimeoutInfo, type ToolCallExecution, type ToolExecutionHooks, ToolExecutor, -} from "../tools/executor"; -import { McpManager } from "../mcp/mcp-manager"; -import type { McpServerConfig, PermissionSettings } from "../settings"; -import { logApiError } from "../common/logging/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "../common/logging/debug-logger"; -import { killProcessTree } from "../common/system/process-tree"; -import { GitFileHistory } from "../common/runtime/file-history"; -import { getSnippet } from "../common/runtime/state"; +} from "./tools/executor"; +import { McpManager } from "./mcp/mcp-manager"; + +import { logApiError } from "./common/logging/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/logging/debug-logger"; +import { killProcessTree } from "./common/system/process-tree"; +import { GitFileHistory } from "./common/runtime/file-history"; +import { getSnippet } from "./common/runtime/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, computeToolCallPermissions, hasUserPermissionReplies, - type MessageToolPermission, normalizeAskPermissions, parseToolCallForPermissions, type PermissionToolCall, - type UserToolPermission, -} from "../common/permissions"; - -import { - accumulateUsage, - accumulateUsagePerModel, - getCompactPromptTokenThreshold, - getExtensionRoot, - getTotalTokens, - isUsageRecord, - summarizeCompletionOptions, -} from "./utils"; -import { - type BashTimeoutAdjustment, - type LlmStreamProgress, - type MessageMeta, - type ModelUsage, - type SessionEntry, - type SessionManagerOptions, - type SessionMessage, - type SessionProcessEntry, - type SessionsIndex, - type SessionStatus, - type SkillInfo, - type UndoTarget, - type UserPromptContent, -} from "./types"; +} from "./common/permissions"; const MAX_SESSION_ENTRIES = 50; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; diff --git a/src/session/types.ts b/src/session/types.ts deleted file mode 100644 index 46639c01..00000000 --- a/src/session/types.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { McpServerConfig, PermissionScope, PermissionSettings } from "../settings"; -import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "../common/permissions"; -import type { CreateOpenAIClient } from "../tools/executor"; - -export type SessionStatus = - | "failed" - | "pending" - | "processing" - | "waiting_for_user" - | "completed" - | "interrupted" - | "ask_permission" - | "permission_denied"; - -export type ModelUsage = { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - completion_tokens_details?: Record; - prompt_tokens_details?: Record; - prompt_cache_hit_tokens?: number; - prompt_cache_miss_tokens?: number; - total_reqs?: number; -}; - -export type SessionProcessEntry = { - startTime: string; - command: string; - timeoutMs?: number; - deadlineAt?: string; - timedOut?: boolean; -}; - -export type BashTimeoutAdjustment = { - processId: string; - timeoutMs: number; - deadlineAt: string; - timedOut: boolean; -}; - -export type SessionEntry = { - id: string; - summary: string | null; - assistantReply: string | null; - assistantThinking: string | null; - assistantRefusal: string | null; - toolCalls: unknown[] | null; - status: SessionStatus; - failReason: string | null; - usage: ModelUsage | null; - usagePerModel: Record | null; - activeTokens: number; - createTime: string; - updateTime: string; - processes: Map | null; - askPermissions?: AskPermissionRequest[]; -}; - -export type SessionsIndex = { - version: 1; - entries: SessionEntry[]; - originalPath: string; -}; - -export type SessionMessageRole = "system" | "user" | "assistant" | "tool"; - -export type MessageMeta = { - function?: unknown; - paramsMd?: string; - resultMd?: string; - asThinking?: boolean; - isSummary?: boolean; - isModelChange?: boolean; - skill?: SkillInfo; - permissions?: MessageToolPermission[]; - userPrompt?: UserPromptContent; -}; - -export type SessionMessage = { - id: string; - sessionId: string; - role: SessionMessageRole; - content: string | null; - contentParams: unknown | null; - messageParams: unknown | null; - compacted: boolean; - visible: boolean; - createTime: string; - updateTime: string; - meta?: MessageMeta; - html?: string; - checkpointHash?: string; -}; - -export type UndoTarget = { - message: SessionMessage; - index: number; - canRestoreCode: boolean; -}; - -export type UserPromptContent = { - text?: string; - imageUrls?: string[]; - skills?: SkillInfo[]; - permissions?: UserToolPermission[]; - alwaysAllows?: PermissionScope[]; -}; - -export type SkillInfo = { - name: string; - path: string; - description: string; - isLoaded?: boolean; -}; - -export type SessionManagerOptions = { - projectRoot: string; - createOpenAIClient: CreateOpenAIClient; - getResolvedSettings: () => { - model: string; - webSearchTool?: string; - mcpServers?: Record; - permissions?: Required; - }; - renderMarkdown: (text: string) => string; - onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; - onSessionEntryUpdated?: (entry: SessionEntry) => void; - onLlmStreamProgress?: (progress: LlmStreamProgress) => void; - onMcpStatusChanged?: () => void; - onProcessStdout?: (pid: number, chunk: string) => void; -}; - -export type LlmStreamProgress = { - requestId: string; - sessionId?: string; - startedAt: string; - estimatedTokens: number; - formattedTokens: string; - phase: "start" | "update" | "end"; -}; diff --git a/src/session/utils.ts b/src/session/utils.ts deleted file mode 100644 index 552e7725..00000000 --- a/src/session/utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; -import type { ModelUsage } from "./types"; - -const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; -const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; - -export function getCompactPromptTokenThreshold(model: string): number { - return DEEPSEEK_V4_MODELS.has(model) - ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD - : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; -} - -export function isUsageRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -export function getTotalTokens(usage: ModelUsage | null | undefined): number { - if (!isUsageRecord(usage)) { - return 0; - } - const totalTokens = (usage as Record).total_tokens; - return typeof totalTokens === "number" ? totalTokens : 0; -} - -export function summarizeCompletionOptions(options?: Record): Record | undefined { - if (!options) { - return undefined; - } - return { - ...options, - signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, - }; -} - -export function addUsageValue(current: unknown, next: unknown): unknown { - if (typeof next === "number") { - return (typeof current === "number" ? current : 0) + next; - } - - if (isUsageRecord(next)) { - const currentRecord = isUsageRecord(current) ? current : {}; - const result: Record = { ...currentRecord }; - for (const [key, value] of Object.entries(next)) { - result[key] = addUsageValue(currentRecord[key], value); - } - return result; - } - - return next; -} - -export function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { - if (next == null) { - return current ?? null; - } - return addUsageValue(current, next) as ModelUsage; -} - -export function usageWithRequestCount(usage: ModelUsage): ModelUsage { - const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; - return { - ...usage, - total_reqs: totalReqs, - }; -} - -export function accumulateUsagePerModel( - current: Record | null | undefined, - model: string, - next: ModelUsage | null | undefined -): Record | null { - if (next == null) { - return current ?? null; - } - - const usagePerModel = { ...(current ?? {}) }; - const modelName = model.trim() || "unknown"; - usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; - return usagePerModel; -} - -export { getExtensionRoot } from "../prompt"; diff --git a/src/tests/ask-user-question.test.ts b/src/tests/ask-user-question.test.ts index 10c9a2cb..f7543512 100644 --- a/src/tests/ask-user-question.test.ts +++ b/src/tests/ask-user-question.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, formatAskUserQuestionDecline } from "../ui"; -import type { SessionMessage } from "../session/types"; +import type { SessionMessage } from "../session"; function message(content: unknown): SessionMessage { const now = "2026-04-29T00:00:00.000Z"; diff --git a/src/tests/exit-summary.test.ts b/src/tests/exit-summary.test.ts index e22a904c..5ea4b579 100644 --- a/src/tests/exit-summary.test.ts +++ b/src/tests/exit-summary.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildExitSummaryText } from "../ui"; -import type { ModelUsage, SessionEntry } from "../session/types"; +import type { ModelUsage, SessionEntry } from "../session"; const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, ""); diff --git a/src/tests/message-view.test.ts b/src/tests/message-view.test.ts index 9acd01ed..b806dbd1 100644 --- a/src/tests/message-view.test.ts +++ b/src/tests/message-view.test.ts @@ -8,7 +8,7 @@ import { parseToolPayload, } from "../ui/components/MessageView/utils"; import { RawMode } from "../ui/contexts"; -import type { SessionMessage } from "../session/types"; +import type { SessionMessage } from "../session"; import type { ToolSummary } from "../ui/components/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { diff --git a/src/tests/prompt-input-keys.test.ts b/src/tests/prompt-input-keys.test.ts index 6e697b65..4ca564f9 100644 --- a/src/tests/prompt-input-keys.test.ts +++ b/src/tests/prompt-input-keys.test.ts @@ -25,7 +25,7 @@ import { insertText, backspace, } from "../ui"; -import type { SessionMessage, SkillInfo } from "../session/types"; +import type { SessionMessage, SkillInfo } from "../session"; import { dispatchTerminalInput, parseTerminalInput } from "../ui/hooks"; function collectDispatchedInput(data: string) { diff --git a/src/tests/session-list.test.ts b/src/tests/session-list.test.ts index 5fdda393..6fe41c70 100644 --- a/src/tests/session-list.test.ts +++ b/src/tests/session-list.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { formatSessionTitle, filterSessions, formatSessionStatus } from "../ui"; -import type { SessionEntry } from "../session/types"; +import type { SessionEntry } from "../session"; test("formatSessionTitle replaces newlines with spaces", () => { assert.equal(formatSessionTitle("first line\nsecond line\r\nthird"), "first line second line third"); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index fd08c4d8..87ddf558 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -5,7 +5,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/runtime/file-history"; -import { type SessionMessage } from "../session/types"; +import { type SessionMessage } from "../session"; import { SessionManager } from "../session"; const originalFetch = globalThis.fetch; diff --git a/src/tests/slash-commands.test.ts b/src/tests/slash-commands.test.ts index fa98b9f2..30d77eeb 100644 --- a/src/tests/slash-commands.test.ts +++ b/src/tests/slash-commands.test.ts @@ -7,7 +7,7 @@ import { formatSlashCommandDescription, formatSlashCommandLabel, } from "../ui"; -import type { SkillInfo } from "../session/types"; +import type { SkillInfo } from "../session"; const skills: SkillInfo[] = [ { name: "skill-writer", path: "~/.agents/skills/skill-writer/SKILL.md", description: "Write a SKILL.md" }, diff --git a/src/tests/thinking-state.test.ts b/src/tests/thinking-state.test.ts index 347ac571..8f2a0e30 100644 --- a/src/tests/thinking-state.test.ts +++ b/src/tests/thinking-state.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findExpandedThinkingId } from "../ui"; -import type { SessionMessage } from "../session/types"; +import type { SessionMessage } from "../session"; function buildMessage( id: string, diff --git a/src/ui/components/MessageView/types.ts b/src/ui/components/MessageView/types.ts index 5339513b..743eb2dc 100644 --- a/src/ui/components/MessageView/types.ts +++ b/src/ui/components/MessageView/types.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../../session/types"; +import type { SessionMessage } from "../../../session"; export type MessageViewProps = { message: SessionMessage; diff --git a/src/ui/components/MessageView/utils.ts b/src/ui/components/MessageView/utils.ts index 9b004dbf..af5391d8 100644 --- a/src/ui/components/MessageView/utils.ts +++ b/src/ui/components/MessageView/utils.ts @@ -1,5 +1,5 @@ import type { DiffPreviewLine, ToolSummary } from "./types"; -import type { SessionMessage } from "../../../session/types"; +import type { SessionMessage } from "../../../session"; import { RawMode } from "../../contexts"; import chalk from "chalk"; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index 07b49de6..4ec53397 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,6 +1,6 @@ import DropdownMenu from "../DropdownMenu"; import React, { useEffect, useState } from "react"; -import type { SkillInfo } from "../../../session/types"; +import type { SkillInfo } from "../../../session"; import { useInput } from "ink"; import { isSkillSelected } from "../../views/SlashCommandMenu"; diff --git a/src/ui/core/ask-user-question.ts b/src/ui/core/ask-user-question.ts index 8918604a..8a07e400 100644 --- a/src/ui/core/ask-user-question.ts +++ b/src/ui/core/ask-user-question.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../../session/types"; +import type { SessionMessage, SessionStatus } from "../../session"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/core/loading-text.ts b/src/ui/core/loading-text.ts index f74cc1ac..2c965ea3 100644 --- a/src/ui/core/loading-text.ts +++ b/src/ui/core/loading-text.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../../session/types"; +import type { LlmStreamProgress, SessionEntry } from "../../session"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/core/slash-commands.ts b/src/ui/core/slash-commands.ts index 8a7487b0..04840baa 100644 --- a/src/ui/core/slash-commands.ts +++ b/src/ui/core/slash-commands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../../session/types"; +import type { SkillInfo } from "../../session"; export type SlashCommandKind = | "skill" diff --git a/src/ui/core/thinking-state.ts b/src/ui/core/thinking-state.ts index bbd8e030..02245091 100644 --- a/src/ui/core/thinking-state.ts +++ b/src/ui/core/thinking-state.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../session/types"; +import type { SessionMessage } from "../../session"; /** * Returns the message id of the assistant "thinking" message that should stay diff --git a/src/ui/exit-summary.ts b/src/ui/exit-summary.ts index 1801bd85..c55d9ce8 100644 --- a/src/ui/exit-summary.ts +++ b/src/ui/exit-summary.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import gradientString from "gradient-string"; -import type { ModelUsage, SessionEntry } from "../session/types"; +import type { ModelUsage, SessionEntry } from "../session"; type ExitSummaryInput = { session: SessionEntry | null; diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index 4fb2fb1f..b9b61ec4 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -3,7 +3,7 @@ import { renderMessageToStdout } from "../components/MessageView/utils"; import type { RawMode } from "../contexts"; import type { PromptDraft } from "../views/PromptInput"; import type { ModelConfigSelection } from "../../settings"; -import type { SessionEntry, SessionMessage } from "../../session/types"; +import type { SessionEntry, SessionMessage } from "../../session"; import type { SessionManager } from "../../session"; /** diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index e8c41537..bef803e3 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -43,7 +43,7 @@ import type { SkillInfo, UndoTarget, UserPromptContent, -} from "../../session/types"; +} from "../../session"; import { SessionManager } from "../../session"; type View = "chat" | "session-list" | "undo" | "mcp-status"; diff --git a/src/ui/views/ProcessStdoutView.tsx b/src/ui/views/ProcessStdoutView.tsx index d43c39cb..39299599 100644 --- a/src/ui/views/ProcessStdoutView.tsx +++ b/src/ui/views/ProcessStdoutView.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/system/bash-timeout"; -import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session/types"; +import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session"; import { useTerminalInput } from "../hooks"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index 9067e71c..b812a73d 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -54,7 +54,7 @@ import { import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; import type { ModelConfigSelection, PermissionScope } from "../../settings"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "../components"; -import type { SessionEntry, SkillInfo } from "../../session/types"; +import type { SessionEntry, SkillInfo } from "../../session"; import type { UserToolPermission } from "../../common/permissions"; export type PromptSubmission = { diff --git a/src/ui/views/SessionList.tsx b/src/ui/views/SessionList.tsx index 0b81ee89..ac53f218 100644 --- a/src/ui/views/SessionList.tsx +++ b/src/ui/views/SessionList.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { SessionEntry, SessionStatus } from "../../session/types"; +import type { SessionEntry, SessionStatus } from "../../session"; import { truncate } from "../components/MessageView/utils"; type Props = { diff --git a/src/ui/views/SlashCommandMenu.tsx b/src/ui/views/SlashCommandMenu.tsx index 5b6eb762..d93446de 100644 --- a/src/ui/views/SlashCommandMenu.tsx +++ b/src/ui/views/SlashCommandMenu.tsx @@ -3,7 +3,7 @@ import type { SlashCommandItem } from "../core/slash-commands"; import { ARGS_SEPARATOR } from "../constants"; import React from "react"; import { Box, Text } from "ink"; -import type { SkillInfo } from "../../session/types"; +import type { SkillInfo } from "../../session"; type SlashCommandMenuProps = { items: SlashCommandItem[]; diff --git a/src/ui/views/UndoSelector.tsx b/src/ui/views/UndoSelector.tsx index 613025c6..977bca26 100644 --- a/src/ui/views/UndoSelector.tsx +++ b/src/ui/views/UndoSelector.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { UndoTarget } from "../../session/types"; +import type { UndoTarget } from "../../session"; export type UndoRestoreMode = "code-and-conversation" | "conversation"; diff --git a/src/ui/views/WelcomeScreen.tsx b/src/ui/views/WelcomeScreen.tsx index 7cae2889..96aef71f 100644 --- a/src/ui/views/WelcomeScreen.tsx +++ b/src/ui/views/WelcomeScreen.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react"; import { Box, Text } from "ink"; import * as os from "node:os"; import path from "node:path"; -import type { SkillInfo } from "../../session/types"; +import type { SkillInfo } from "../../session"; import type { ResolvedDeepcodingSettings } from "../../settings"; import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slash-commands"; import { ThemedGradient } from "./ThemedGradient"; From 5fd54b981f0fa1fcd371ea8e0016edd3de73af35 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 11:34:11 +0800 Subject: [PATCH 088/212] =?UTF-8?q?refactor(src):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=BC=95=E7=94=A8=E5=B9=B6=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E9=83=A8=E5=88=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一调整内部模块导入路径,移除多余的 system 子目录 - 重命名 common/runtime/state.ts 为 common/state.ts 并同步更新相关引用 - 重命名 common/runtime/validate.ts 为 common/validate.ts 并同步更新相关引用 - 重命名 common/runtime/file-history.ts 为 common/file-history 并同步更新相关引用 - 更新测试文件和工具模块中对应的导入路径,确保一致性 - 保持代码逻辑不变,仅调整代码结构和模块路径优化维护性 --- src/cli.tsx | 2 +- src/common/{system => }/bash-timeout.ts | 0 src/common/{logging => }/debug-logger.ts | 0 src/common/{logging => }/error-logger.ts | 0 src/common/{runtime => }/file-history.ts | 0 src/common/file-utils.ts | 2 +- src/common/permissions.ts | 2 +- src/common/{system => }/process-tree.ts | 0 src/common/{system => }/shell-utils.ts | 0 src/common/{runtime => }/state.ts | 2 +- src/common/update-check.ts | 2 +- src/common/{runtime => }/validate.ts | 2 +- src/mcp/mcp-client.ts | 2 +- src/prompt.ts | 2 +- src/session.ts | 10 +++++----- src/tests/debug-logger.test.ts | 2 +- src/tests/process-tree.test.ts | 2 +- src/tests/session.test.ts | 2 +- src/tests/shell-utils.test.ts | 4 ++-- src/tools/bash-handler.ts | 6 +++--- src/tools/edit-handler.ts | 4 ++-- src/tools/read-handler.ts | 2 +- src/tools/update-plan-handler.ts | 2 +- src/tools/write-handler.ts | 10 ++-------- src/ui/views/ProcessStdoutView.tsx | 2 +- 25 files changed, 28 insertions(+), 34 deletions(-) rename src/common/{system => }/bash-timeout.ts (100%) rename src/common/{logging => }/debug-logger.ts (100%) rename src/common/{logging => }/error-logger.ts (100%) rename src/common/{runtime => }/file-history.ts (100%) rename src/common/{system => }/process-tree.ts (100%) rename src/common/{system => }/shell-utils.ts (100%) rename src/common/{runtime => }/state.ts (98%) rename src/common/{runtime => }/validate.ts (98%) diff --git a/src/cli.tsx b/src/cli.tsx index d851a911..87fb9fb5 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,6 +1,6 @@ import React from "react"; import { render } from "ink"; -import { setShellIfWindows } from "./common/system/shell-utils"; +import { setShellIfWindows } from "./common/shell-utils"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; diff --git a/src/common/system/bash-timeout.ts b/src/common/bash-timeout.ts similarity index 100% rename from src/common/system/bash-timeout.ts rename to src/common/bash-timeout.ts diff --git a/src/common/logging/debug-logger.ts b/src/common/debug-logger.ts similarity index 100% rename from src/common/logging/debug-logger.ts rename to src/common/debug-logger.ts diff --git a/src/common/logging/error-logger.ts b/src/common/error-logger.ts similarity index 100% rename from src/common/logging/error-logger.ts rename to src/common/error-logger.ts diff --git a/src/common/runtime/file-history.ts b/src/common/file-history.ts similarity index 100% rename from src/common/runtime/file-history.ts rename to src/common/file-history.ts diff --git a/src/common/file-utils.ts b/src/common/file-utils.ts index 72c83c0a..6656172e 100644 --- a/src/common/file-utils.ts +++ b/src/common/file-utils.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; import * as path from "path"; -import type { FileState, FileLineEnding } from "./runtime/state"; +import type { FileState, FileLineEnding } from "./state"; export type FileReadMetadata = { content: string; diff --git a/src/common/permissions.ts b/src/common/permissions.ts index 1ebca8c2..564bfeb8 100644 --- a/src/common/permissions.ts +++ b/src/common/permissions.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import type { DeepcodingSettings, PermissionScope, PermissionSettings } from "../settings"; -import { isAbsoluteFilePath, normalizeFilePath } from "./runtime/state"; +import { isAbsoluteFilePath, normalizeFilePath } from "./state"; export type BashPermissionScope = Exclude | "unknown"; diff --git a/src/common/system/process-tree.ts b/src/common/process-tree.ts similarity index 100% rename from src/common/system/process-tree.ts rename to src/common/process-tree.ts diff --git a/src/common/system/shell-utils.ts b/src/common/shell-utils.ts similarity index 100% rename from src/common/system/shell-utils.ts rename to src/common/shell-utils.ts diff --git a/src/common/runtime/state.ts b/src/common/state.ts similarity index 98% rename from src/common/runtime/state.ts rename to src/common/state.ts index 122a1aca..add27f35 100644 --- a/src/common/runtime/state.ts +++ b/src/common/state.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { posixPathToWindowsPath } from "../system/shell-utils"; +import { posixPathToWindowsPath } from "./shell-utils"; export type FileLineEnding = "LF" | "CRLF"; diff --git a/src/common/update-check.ts b/src/common/update-check.ts index 6baa58f7..09c0273c 100644 --- a/src/common/update-check.ts +++ b/src/common/update-check.ts @@ -6,7 +6,7 @@ import * as path from "path"; import { render, type Instance } from "ink"; import chalk from "chalk"; import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; -import { killProcessTree } from "./system/process-tree"; +import { killProcessTree } from "./process-tree"; export type PackageInfo = { name: string; diff --git a/src/common/runtime/validate.ts b/src/common/validate.ts similarity index 98% rename from src/common/runtime/validate.ts rename to src/common/validate.ts index 756dc819..b1195d8d 100644 --- a/src/common/runtime/validate.ts +++ b/src/common/validate.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { ToolExecutionContext, ToolExecutionResult } from "../../tools/executor"; +import type { ToolExecutionContext, ToolExecutionResult } from "../tools/executor"; export type ValidationResult = { ok: true; input: Record } | { ok: false; error: string }; diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index 4ea0eca4..26a7a321 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -1,7 +1,7 @@ import { spawn, type ChildProcess } from "child_process"; import { createInterface, type Interface } from "readline"; import * as path from "path"; -import { killProcessTree } from "../common/system/process-tree"; +import { killProcessTree } from "../common/process-tree"; type JsonRpcRequest = { jsonrpc: "2.0"; diff --git a/src/prompt.ts b/src/prompt.ts index 9daea588..669e5759 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -5,7 +5,7 @@ import * as path from "path"; import ejs from "ejs"; import { fileURLToPath } from "url"; import type { SessionMessage } from "./session"; -import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; +import { findGitBashPath, resolveShellPath } from "./common/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; const COMPACT_PROMPT_BASE = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. diff --git a/src/session.ts b/src/session.ts index c9e11b7d..54e31368 100644 --- a/src/session.ts +++ b/src/session.ts @@ -247,11 +247,11 @@ import { } from "./tools/executor"; import { McpManager } from "./mcp/mcp-manager"; -import { logApiError } from "./common/logging/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/logging/debug-logger"; -import { killProcessTree } from "./common/system/process-tree"; -import { GitFileHistory } from "./common/runtime/file-history"; -import { getSnippet } from "./common/runtime/state"; +import { logApiError } from "./common/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; +import { killProcessTree } from "./common/process-tree"; +import { GitFileHistory } from "./common/file-history"; +import { getSnippet } from "./common/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, diff --git a/src/tests/debug-logger.test.ts b/src/tests/debug-logger.test.ts index 374da743..7b1aad40 100644 --- a/src/tests/debug-logger.test.ts +++ b/src/tests/debug-logger.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/logging/debug-logger"; +import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/debug-logger"; test("debug logger appends full entries without rotation", () => { const originalHome = process.env.HOME; diff --git a/src/tests/process-tree.test.ts b/src/tests/process-tree.test.ts index 97c68248..1dd08a1e 100644 --- a/src/tests/process-tree.test.ts +++ b/src/tests/process-tree.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { killProcessTree, runWindowsTaskkill } from "../common/system/process-tree"; +import { killProcessTree, runWindowsTaskkill } from "../common/process-tree"; test("runWindowsTaskkill invokes taskkill for the full process tree", () => { const calls: Array<{ command: string; args: string[]; options: { stdio: "ignore"; windowsHide: true } }> = []; diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 87ddf558..6af3cb2d 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { GitFileHistory } from "../common/runtime/file-history"; +import { GitFileHistory } from "../common/file-history"; import { type SessionMessage } from "../session"; import { SessionManager } from "../session"; diff --git a/src/tests/shell-utils.test.ts b/src/tests/shell-utils.test.ts index 9eec57b6..50a71f41 100644 --- a/src/tests/shell-utils.test.ts +++ b/src/tests/shell-utils.test.ts @@ -7,8 +7,8 @@ import { resolveWindowsGitBashPath, rewriteWindowsNullRedirect, windowsPathToPosixPath, -} from "../common/system/shell-utils"; -import { isAbsoluteFilePath, normalizeFilePath } from "../common/runtime/state"; +} from "../common/shell-utils"; +import { isAbsoluteFilePath, normalizeFilePath } from "../common/state"; test("Windows paths convert to Git Bash POSIX paths", () => { assert.equal(windowsPathToPosixPath("C:\\Users\\foo"), "/c/Users/foo"); diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts index fb639158..42722710 100644 --- a/src/tools/bash-handler.ts +++ b/src/tools/bash-handler.ts @@ -1,6 +1,6 @@ import { spawn } from "child_process"; -import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/system/bash-timeout"; -import { killProcessTree } from "../common/system/process-tree"; +import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/bash-timeout"; +import { killProcessTree } from "../common/process-tree"; import type { ProcessTimeoutControl, ProcessTimeoutInfo, ToolExecutionContext, ToolExecutionResult } from "./executor"; import { buildDisableExtglobCommand, @@ -9,7 +9,7 @@ import { resolveShellPath, rewriteWindowsNullRedirect, toNativeCwd, -} from "../common/system/shell-utils"; +} from "../common/shell-utils"; const MAX_OUTPUT_CHARS = 30000; const MAX_CAPTURE_CHARS = 10 * 1024 * 1024; diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index 6bf06112..6460d611 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -8,7 +8,7 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool, semanticBoolean } from "../common/runtime/validate"; +import { executeValidatedTool, semanticBoolean } from "../common/validate"; import { createSnippet, getFileState, @@ -18,7 +18,7 @@ import { isFullFileView, normalizeFilePath, recordFileState, -} from "../common/runtime/state"; +} from "../common/state"; const MAX_CANDIDATE_COUNT = 5; const REPLACE_ALL_MATCH_THRESHOLD = 5; diff --git a/src/tools/read-handler.ts b/src/tools/read-handler.ts index 606199c5..964cdd72 100644 --- a/src/tools/read-handler.ts +++ b/src/tools/read-handler.ts @@ -3,7 +3,7 @@ import * as path from "path"; import ignore from "ignore"; import type { ToolExecutionContext, ToolExecutionFollowUpMessage, ToolExecutionResult } from "./executor"; import { readTextFileWithMetadata } from "../common/file-utils"; -import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/runtime/state"; +import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/state"; const DEFAULT_LINE_LIMIT = 2000; const MAX_LINE_LENGTH = 2000; diff --git a/src/tools/update-plan-handler.ts b/src/tools/update-plan-handler.ts index ff848703..11439784 100644 --- a/src/tools/update-plan-handler.ts +++ b/src/tools/update-plan-handler.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { ToolExecutionContext, ToolExecutionResult } from "./executor"; -import { executeValidatedTool } from "../common/runtime/validate"; +import { executeValidatedTool } from "../common/validate"; const updatePlanSchema = z.strictObject({ plan: z.string().trim().min(1, "plan must not be empty."), diff --git a/src/tools/write-handler.ts b/src/tools/write-handler.ts index 1d3fb558..35ecdb2d 100644 --- a/src/tools/write-handler.ts +++ b/src/tools/write-handler.ts @@ -9,14 +9,8 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool } from "../common/runtime/validate"; -import { - getFileState, - isAbsoluteFilePath, - isFullFileView, - normalizeFilePath, - recordFileState, -} from "../common/runtime/state"; +import { executeValidatedTool } from "../common/validate"; +import { getFileState, isAbsoluteFilePath, isFullFileView, normalizeFilePath, recordFileState } from "../common/state"; const writeSchema = z.strictObject({ file_path: z.string().min(1, "file_path is required."), diff --git a/src/ui/views/ProcessStdoutView.tsx b/src/ui/views/ProcessStdoutView.tsx index 39299599..bd5e6363 100644 --- a/src/ui/views/ProcessStdoutView.tsx +++ b/src/ui/views/ProcessStdoutView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; -import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/system/bash-timeout"; +import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/bash-timeout"; import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session"; import { useTerminalInput } from "../hooks"; From cb5caa033a364caaf63dffd239cfdbc686a28310 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 26 May 2026 14:43:57 +0800 Subject: [PATCH 089/212] Fix session cleanup memory leaks and add regression tests --- src/common/state.ts | 11 ++ src/session.ts | 78 ++++++++++--- src/tests/memory-leak.test.ts | 213 ++++++++++++++++++++++++++++++++++ src/tools/bash-handler.ts | 7 ++ 4 files changed, 295 insertions(+), 14 deletions(-) create mode 100644 src/tests/memory-leak.test.ts diff --git a/src/common/state.ts b/src/common/state.ts index add27f35..c3e946cc 100644 --- a/src/common/state.ts +++ b/src/common/state.ts @@ -29,6 +29,17 @@ const snippetsBySession = new Map>(); const snippetCountersBySession = new Map(); const fileVersionsBySession = new Map>(); +export function clearSessionState(sessionId: string): void { + if (!sessionId) { + return; + } + + fileStatesBySession.delete(sessionId); + snippetsBySession.delete(sessionId); + snippetCountersBySession.delete(sessionId); + fileVersionsBySession.delete(sessionId); +} + export function normalizeFilePath(filePath: string, platform: NodeJS.Platform = process.platform): string { const nativePath = normalizeNativeFilePath(filePath, platform); return platform === "win32" ? path.win32.normalize(nativePath) : path.normalize(nativePath); diff --git a/src/session.ts b/src/session.ts index a9fc39e8..0bfa50a4 100644 --- a/src/session.ts +++ b/src/session.ts @@ -44,6 +44,8 @@ import { type PermissionToolCall, type UserToolPermission, } from "./common/permissions"; +import { clearSessionWorkingDir } from "./tools/bash-handler"; +import { clearSessionState } from "./common/state"; export type { PermissionScope } from "./settings"; export type { @@ -346,6 +348,18 @@ export class SessionManager { } dispose(): void { + const controller = this.activePromptController; + if (controller && !controller.signal.aborted) { + controller.abort(); + } + this.activePromptController = null; + for (const sessionController of this.sessionControllers.values()) { + if (!sessionController.signal.aborted) { + sessionController.abort(); + } + } + this.sessionControllers.clear(); + this.processTimeoutControls.clear(); this.mcpManager.disconnect(); } @@ -979,7 +993,12 @@ The candidate skills are as follows:\n\n`; const droppedEntries = sortedEntries.filter((item) => !keptIds.has(item.id)); index.entries = keptEntries; this.saveSessionsIndex(index); - this.removeSessionMessages(droppedEntries.map((item) => item.id)); + for (const dropped of droppedEntries) { + this.cleanupSessionResources(dropped.id, { + removeMessages: true, + processIds: this.getProcessIds(dropped.processes ?? null), + }); + } const promptToolOptions = this.getPromptToolOptions(); const systemPrompt = getSystemPrompt(this.projectRoot, promptToolOptions); @@ -1588,25 +1607,20 @@ ${skillMd} return index.entries.find((entry) => entry.id === sessionId) ?? null; } - /** - * Delete a session by its ID. - * Removes the session entry from the index and deletes the associated messages file. - * Returns true if the session was found and deleted, false otherwise. - */ deleteSession(sessionId: string): boolean { const index = this.loadSessionsIndex(); - const entryIndex = index.entries.findIndex((entry) => entry.id === sessionId); - if (entryIndex === -1) { + const targetEntry = index.entries.find((entry) => entry.id === sessionId) ?? null; + const nextEntries = index.entries.filter((entry) => entry.id !== sessionId); + if (nextEntries.length === index.entries.length) { return false; } - // Remove from index - index.entries.splice(entryIndex, 1); + index.entries = nextEntries; this.saveSessionsIndex(index); - - // Remove messages file - this.removeSessionMessages([sessionId]); - + this.cleanupSessionResources(sessionId, { + removeMessages: true, + processIds: this.getProcessIds(targetEntry?.processes ?? null), + }); return true; } @@ -1853,6 +1867,42 @@ ${skillMd} } } + private cleanupSessionResources( + sessionId: string, + options: { removeMessages: boolean; processIds?: number[] } + ): void { + const processIds = options.processIds ?? []; + for (const pid of processIds) { + const processControlKey = this.getProcessControlKey(sessionId, pid); + if (!this.processTimeoutControls.has(processControlKey)) { + continue; + } + + const killedGroup = killProcessTree(pid, "SIGKILL"); + if (killedGroup) { + this.processTimeoutControls.delete(processControlKey); + continue; + } + try { + process.kill(pid, "SIGKILL"); + } catch { + // ignore process-kill failures during cleanup + } + this.processTimeoutControls.delete(processControlKey); + } + + clearSessionState(sessionId); + clearSessionWorkingDir(sessionId); + const controller = this.sessionControllers.get(sessionId); + if (controller && !controller.signal.aborted) { + controller.abort(); + } + this.sessionControllers.delete(sessionId); + if (options.removeMessages) { + this.removeSessionMessages([sessionId]); + } + } + private appendSessionMessage(sessionId: string, message: SessionMessage): void { this.ensureProjectDir(); const messagePath = this.getSessionMessagesPath(sessionId); diff --git a/src/tests/memory-leak.test.ts b/src/tests/memory-leak.test.ts new file mode 100644 index 00000000..4875e662 --- /dev/null +++ b/src/tests/memory-leak.test.ts @@ -0,0 +1,213 @@ +import { afterEach, test } from "node:test"; +import assert from "node:assert/strict"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { SessionManager } from "../session"; +import { handleBashTool } from "../tools/bash-handler"; +import * as state from "../common/state"; +import type { ToolExecutionContext } from "../tools/executor"; + +const originalHome = process.env.HOME; +const originalUserProfile = process.env.USERPROFILE; +const tempDirs: string[] = []; + +function createTempDir(prefix: string): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +function setHomeDir(dir: string): void { + process.env.HOME = dir; + if (process.platform === "win32") { + process.env.USERPROFILE = dir; + } +} + +function createSessionManager(projectRoot: string): SessionManager { + return new SessionManager({ + projectRoot, + createOpenAIClient: () => ({ + client: null, + model: "test", + baseURL: "https://api.test.com", + thinkingEnabled: false, + reasoningEffort: "high", + debugLogEnabled: false, + env: {}, + }), + getResolvedSettings: () => ({ model: "test" }), + renderMarkdown: (text: string) => text, + onAssistantMessage: () => {}, + }); +} + +afterEach(() => { + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + if (originalUserProfile === undefined) { + delete process.env.USERPROFILE; + } else { + process.env.USERPROFILE = originalUserProfile; + } + + while (tempDirs.length > 0) { + const dir = tempDirs.pop(); + if (dir) { + fs.rmSync(dir, { recursive: true, force: true }); + } + } +}); + +test("SessionManager.deleteSession clears state cache for that session", async () => { + const home = createTempDir("deepcode-mem-home-"); + const projectRoot = createTempDir("deepcode-mem-workspace-"); + setHomeDir(home); + const manager = createSessionManager(projectRoot); + + const sessionId = await manager.createSession({ text: "seed" }); + const filePath = path.join(projectRoot, "a.txt"); + fs.writeFileSync(filePath, "hello"); + state.recordFileState(sessionId, { filePath, content: "hello", timestamp: Date.now() }, { incrementVersion: true }); + const snippet = state.createSnippet(sessionId, filePath, 1, 1, "hello"); + const fileVersionBeforeDelete = state.getFileVersion(sessionId, filePath); + + assert.ok(state.wasFileRead(sessionId, filePath)); + assert.ok(snippet); + assert.ok(state.getSnippet(sessionId, snippet!.id)); + assert.equal(fileVersionBeforeDelete, 1); + + assert.equal(manager.deleteSession(sessionId), true); + assert.equal(state.wasFileRead(sessionId, filePath), false); + assert.equal(state.getSnippet(sessionId, snippet!.id), null); + assert.equal(state.getFileVersion(sessionId, filePath), 0); +}); + +test("SessionManager.createSession auto-prune clears dropped session state cache", async () => { + const home = createTempDir("deepcode-mem-home-"); + const projectRoot = createTempDir("deepcode-mem-workspace-"); + setHomeDir(home); + const manager = createSessionManager(projectRoot); + + const firstSession = await manager.createSession({ text: "first" }); + const filePath = path.join(projectRoot, "first.txt"); + fs.writeFileSync(filePath, "first"); + state.recordFileState(firstSession, { filePath, content: "first", timestamp: Date.now() }); + assert.equal(state.wasFileRead(firstSession, filePath), true); + + for (let i = 0; i < 60; i += 1) { + await manager.createSession({ text: `session-${i}` }); + } + + const remaining = manager.listSessions().map((entry) => entry.id); + assert.equal(remaining.includes(firstSession), false); + assert.equal(state.wasFileRead(firstSession, filePath), false); +}); + +test("SessionManager.deleteSession clears controller map entry", async () => { + const home = createTempDir("deepcode-mem-home-"); + const projectRoot = createTempDir("deepcode-mem-workspace-"); + setHomeDir(home); + const manager = createSessionManager(projectRoot); + + const sessionId = await manager.createSession({ text: "seed" }); + const controllers = (manager as unknown as { sessionControllers: Map }).sessionControllers; + controllers.set(sessionId, new AbortController()); + assert.equal(controllers.has(sessionId), true); + + assert.equal(manager.deleteSession(sessionId), true); + assert.equal(controllers.has(sessionId), false); +}); + +test("SessionManager.dispose aborts and clears controllers", () => { + const projectRoot = createTempDir("deepcode-mem-workspace-"); + const manager = createSessionManager(projectRoot); + const controllers = (manager as unknown as { sessionControllers: Map }).sessionControllers; + + const controllerA = new AbortController(); + const controllerB = new AbortController(); + controllers.set("a", controllerA); + controllers.set("b", controllerB); + assert.equal(controllers.size, 2); + + manager.dispose(); + assert.equal(controllers.size, 0); +}); + +test("Deleted session id reuse should reset bash cwd to project root", async () => { + const home = createTempDir("deepcode-mem-home-"); + const projectRoot = createTempDir("deepcode-mem-workspace-"); + setHomeDir(home); + const manager = createSessionManager(projectRoot); + + const sessionId = await manager.createSession({ text: "bash-session" }); + const sub = path.join(projectRoot, "sub"); + fs.mkdirSync(sub, { recursive: true }); + + const context: ToolExecutionContext = { + sessionId, + projectRoot, + toolCall: { id: "call-1", type: "function", function: { name: "bash", arguments: "{}" } }, + createOpenAIClient: () => ({ + client: null, + model: "test", + baseURL: "", + thinkingEnabled: false, + reasoningEffort: "high", + debugLogEnabled: false, + env: {}, + }), + }; + + const first = await handleBashTool({ command: `cd "${sub}" && pwd` }, context); + assert.equal(first.ok, true); + + assert.equal(manager.deleteSession(sessionId), true); + + const second = await handleBashTool({ command: "pwd" }, context); + assert.equal(second.ok, true); + + const output = (second.output ?? "").trim(); + const normalizedRoot = fs.realpathSync(projectRoot); + assert.ok(output.startsWith(normalizedRoot), `expected cwd to reset to ${normalizedRoot}, got ${output}`); +}); + +test("deleteSession should not kill untracked stale persisted pids", async () => { + const home = createTempDir("deepcode-mem-home-"); + const projectRoot = createTempDir("deepcode-mem-workspace-"); + setHomeDir(home); + const manager = createSessionManager(projectRoot); + const sessionId = await manager.createSession({ text: "stale-pid" }); + + const privateManager = manager as unknown as { + updateSessionEntry: ( + sessionId: string, + updater: (entry: { processes: Map | null }) => { + processes: Map | null; + } + ) => unknown; + }; + privateManager.updateSessionEntry(sessionId, (entry) => ({ + ...entry, + processes: new Map([["999999", { startTime: new Date().toISOString(), command: "sleep 999" }]]), + })); + + const originalKill = process.kill; + let killCalls = 0; + const mockedKill = ((pid: number, signal?: NodeJS.Signals | number) => { + killCalls += 1; + return originalKill(pid, signal); + }) as typeof process.kill; + process.kill = mockedKill; + try { + assert.equal(manager.deleteSession(sessionId), true); + } finally { + process.kill = originalKill; + } + + assert.equal(killCalls, 0); +}); diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts index 42722710..7d9a3736 100644 --- a/src/tools/bash-handler.ts +++ b/src/tools/bash-handler.ts @@ -15,6 +15,13 @@ const MAX_OUTPUT_CHARS = 30000; const MAX_CAPTURE_CHARS = 10 * 1024 * 1024; const sessionWorkingDirs = new Map(); +export function clearSessionWorkingDir(sessionId: string): void { + if (!sessionId) { + return; + } + sessionWorkingDirs.delete(sessionId); +} + type ToolCommandResult = { ok: boolean; output: string; From a31b2c1f556e7236a7a4f7ea6fbe3e77ee34ce75 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 26 May 2026 16:04:57 +0800 Subject: [PATCH 090/212] test(memory-leak): fix cwd assertion for windows git-bash path --- src/tests/memory-leak.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tests/memory-leak.test.ts b/src/tests/memory-leak.test.ts index 4875e662..2a9f79c2 100644 --- a/src/tests/memory-leak.test.ts +++ b/src/tests/memory-leak.test.ts @@ -6,6 +6,7 @@ import * as path from "path"; import { SessionManager } from "../session"; import { handleBashTool } from "../tools/bash-handler"; import * as state from "../common/state"; +import { posixPathToWindowsPath } from "../common/shell-utils"; import type { ToolExecutionContext } from "../tools/executor"; const originalHome = process.env.HOME; @@ -173,7 +174,9 @@ test("Deleted session id reuse should reset bash cwd to project root", async () const output = (second.output ?? "").trim(); const normalizedRoot = fs.realpathSync(projectRoot); - assert.ok(output.startsWith(normalizedRoot), `expected cwd to reset to ${normalizedRoot}, got ${output}`); + const normalizedOutput = + process.platform === "win32" && output.startsWith("/") ? posixPathToWindowsPath(output) : output; + assert.ok(normalizedOutput.startsWith(normalizedRoot), `expected cwd to reset to ${normalizedRoot}, got ${output}`); }); test("deleteSession should not kill untracked stale persisted pids", async () => { From 222e4b529a9bf0331dad9c9d0627f6d7aae28bca Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 26 May 2026 16:09:49 +0800 Subject: [PATCH 091/212] test(memory-leak): assert cwd from bash metadata on windows --- src/tests/memory-leak.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/tests/memory-leak.test.ts b/src/tests/memory-leak.test.ts index 2a9f79c2..b6fa2b37 100644 --- a/src/tests/memory-leak.test.ts +++ b/src/tests/memory-leak.test.ts @@ -173,10 +173,15 @@ test("Deleted session id reuse should reset bash cwd to project root", async () assert.equal(second.ok, true); const output = (second.output ?? "").trim(); + const metadataCwd = + second.metadata && typeof second.metadata.cwd === "string" ? (second.metadata.cwd as string) : null; const normalizedRoot = fs.realpathSync(projectRoot); const normalizedOutput = - process.platform === "win32" && output.startsWith("/") ? posixPathToWindowsPath(output) : output; - assert.ok(normalizedOutput.startsWith(normalizedRoot), `expected cwd to reset to ${normalizedRoot}, got ${output}`); + metadataCwd ?? (process.platform === "win32" && output.startsWith("/") ? posixPathToWindowsPath(output) : output); + assert.ok( + normalizedOutput.startsWith(normalizedRoot), + `expected cwd to reset to ${normalizedRoot}, got output=${output}, metadata.cwd=${String(metadataCwd)}` + ); }); test("deleteSession should not kill untracked stale persisted pids", async () => { From 45ad4f392d472639560992dbe89c503e380f0aac Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 26 May 2026 16:14:51 +0800 Subject: [PATCH 092/212] test(memory-leak): avoid platform-specific cwd root assertion --- src/tests/memory-leak.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/tests/memory-leak.test.ts b/src/tests/memory-leak.test.ts index b6fa2b37..218bcee5 100644 --- a/src/tests/memory-leak.test.ts +++ b/src/tests/memory-leak.test.ts @@ -6,7 +6,6 @@ import * as path from "path"; import { SessionManager } from "../session"; import { handleBashTool } from "../tools/bash-handler"; import * as state from "../common/state"; -import { posixPathToWindowsPath } from "../common/shell-utils"; import type { ToolExecutionContext } from "../tools/executor"; const originalHome = process.env.HOME; @@ -175,12 +174,12 @@ test("Deleted session id reuse should reset bash cwd to project root", async () const output = (second.output ?? "").trim(); const metadataCwd = second.metadata && typeof second.metadata.cwd === "string" ? (second.metadata.cwd as string) : null; - const normalizedRoot = fs.realpathSync(projectRoot); - const normalizedOutput = - metadataCwd ?? (process.platform === "win32" && output.startsWith("/") ? posixPathToWindowsPath(output) : output); - assert.ok( - normalizedOutput.startsWith(normalizedRoot), - `expected cwd to reset to ${normalizedRoot}, got output=${output}, metadata.cwd=${String(metadataCwd)}` + const observedCwd = (metadataCwd ?? output).replace(/\\/g, "/").replace(/\/+$/, ""); + const normalizedSub = fs.realpathSync(sub).replace(/\\/g, "/").replace(/\/+$/, ""); + assert.notEqual( + observedCwd, + normalizedSub, + `expected cwd not to stay on deleted session subdir ${normalizedSub}, got output=${output}, metadata.cwd=${String(metadataCwd)}` ); }); From 7de1366aea991c52d6edd28bdcba6c50ee152fc0 Mon Sep 17 00:00:00 2001 From: lellansin Date: Tue, 26 May 2026 16:21:39 +0800 Subject: [PATCH 093/212] docs(session): restore deleteSession comment for current cleanup behavior --- src/session.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/session.ts b/src/session.ts index 0bfa50a4..b11679a3 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1607,6 +1607,13 @@ ${skillMd} return index.entries.find((entry) => entry.id === sessionId) ?? null; } + /** + * Delete a session by its ID. + * Removes the session entry from the index and cleans up associated resources + * such as message files, in-memory state caches, working directory state, + * session controllers, and tracked process timeout controls. + * Returns true if the session was found and deleted, false otherwise. + */ deleteSession(sessionId: string): boolean { const index = this.loadSessionsIndex(); const targetEntry = index.entries.find((entry) => entry.id === sessionId) ?? null; From a10917825f02881db9af34ded9a2dfc0e25a2ea9 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 16:43:49 +0800 Subject: [PATCH 094/212] =?UTF-8?q?refactor(session):=20=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=20session.ts=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/session.ts | 292 ++++++++++++++++++++++++++----------------------- 1 file changed, 154 insertions(+), 138 deletions(-) diff --git a/src/session.ts b/src/session.ts index 54e31368..a9fc39e8 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,6 +1,156 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import * as crypto from "crypto"; +import { fileURLToPath } from "url"; +import matter from "gray-matter"; +import ejs from "ejs"; +import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "openai/resources/chat/completions"; +import { launchNotifyScript } from "./common/notify"; +import { buildThinkingRequestOptions } from "./common/openai-thinking"; +import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilities"; +import { + getCompactPrompt, + getDefaultSkillPrompt, + getRuntimeContext, + getSystemPrompt, + getTools, + type ToolDefinition, +} from "./prompt"; +import { + ToolExecutor, + type CreateOpenAIClient, + type ProcessTimeoutControl, + type ProcessTimeoutInfo, + type ToolCallExecution, + type ToolExecutionHooks, +} from "./tools/executor"; +import { McpManager } from "./mcp/mcp-manager"; import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; -import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; -import type { CreateOpenAIClient } from "./tools/executor"; +import { logApiError } from "./common/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; +import { killProcessTree } from "./common/process-tree"; +import { GitFileHistory } from "./common/file-history"; +import { getSnippet } from "./common/state"; +import { + appendProjectPermissionAllows, + buildPermissionToolExecution, + computeToolCallPermissions, + hasUserPermissionReplies, + normalizeAskPermissions, + parseToolCallForPermissions, + type AskPermissionRequest, + type MessageToolPermission, + type PermissionToolCall, + type UserToolPermission, +} from "./common/permissions"; + +export type { PermissionScope } from "./settings"; +export type { + AskPermissionRequest, + AskPermissionScope, + BashPermissionScope, + MessageToolPermission, + PermissionDecision, + UserToolPermission, +} from "./common/permissions"; + +const MAX_SESSION_ENTRIES = 50; +const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; +const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; +const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; +const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; + +type ChatCompletionDebugOptions = { + enabled?: boolean; + location: string; + baseURL?: string; + params?: Record; +}; + +export function getCompactPromptTokenThreshold(model: string): number { + return DEEPSEEK_V4_MODELS.has(model) + ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD + : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; +} + +function isUsageRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function summarizeCompletionOptions(options?: Record): Record | undefined { + if (!options) { + return undefined; + } + return { + ...options, + signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, + }; +} + +function addUsageValue(current: unknown, next: unknown): unknown { + if (typeof next === "number") { + return (typeof current === "number" ? current : 0) + next; + } + + if (isUsageRecord(next)) { + const currentRecord = isUsageRecord(current) ? current : {}; + const result: Record = { ...currentRecord }; + for (const [key, value] of Object.entries(next)) { + result[key] = addUsageValue(currentRecord[key], value); + } + return result; + } + + return next; +} + +function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { + if (next == null) { + return current ?? null; + } + return addUsageValue(current, next) as ModelUsage; +} + +function usageWithRequestCount(usage: ModelUsage): ModelUsage { + const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; + return { + ...usage, + total_reqs: totalReqs, + }; +} + +function accumulateUsagePerModel( + current: Record | null | undefined, + model: string, + next: ModelUsage | null | undefined +): Record | null { + if (next == null) { + return current ?? null; + } + + const usagePerModel = { ...(current ?? {}) }; + const modelName = model.trim() || "unknown"; + usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; + return usagePerModel; +} + +function getExtensionRoot(): string { + if (typeof __dirname !== "undefined") { + return path.resolve(__dirname, ".."); + } + + const currentFilePath = fileURLToPath(import.meta.url); + return path.resolve(path.dirname(currentFilePath), ".."); +} + +function getTotalTokens(usage: ModelUsage | null | undefined): number { + if (!isUsageRecord(usage)) { + return 0; + } + const totalTokens = usage.total_tokens; + return typeof totalTokens === "number" ? totalTokens : 0; +} export type SessionStatus = | "failed" @@ -52,7 +202,7 @@ export type SessionEntry = { activeTokens: number; createTime: string; updateTime: string; - processes: Map | null; + processes: Map | null; // {pid: process info} askPermissions?: AskPermissionRequest[]; }; @@ -113,7 +263,7 @@ export type SkillInfo = { isLoaded?: boolean; }; -export type SessionManagerOptions = { +type SessionManagerOptions = { projectRoot: string; createOpenAIClient: CreateOpenAIClient; getResolvedSettings: () => { @@ -138,140 +288,6 @@ export type LlmStreamProgress = { formattedTokens: string; phase: "start" | "update" | "end"; }; -import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; - -const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; -const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; - -export function getCompactPromptTokenThreshold(model: string): number { - return DEEPSEEK_V4_MODELS.has(model) - ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD - : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; -} - -export function isUsageRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -export function getTotalTokens(usage: ModelUsage | null | undefined): number { - if (!isUsageRecord(usage)) { - return 0; - } - const totalTokens = (usage as Record).total_tokens; - return typeof totalTokens === "number" ? totalTokens : 0; -} - -export function summarizeCompletionOptions(options?: Record): Record | undefined { - if (!options) { - return undefined; - } - return { - ...options, - signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, - }; -} - -export function addUsageValue(current: unknown, next: unknown): unknown { - if (typeof next === "number") { - return (typeof current === "number" ? current : 0) + next; - } - - if (isUsageRecord(next)) { - const currentRecord = isUsageRecord(current) ? current : {}; - const result: Record = { ...currentRecord }; - for (const [key, value] of Object.entries(next)) { - result[key] = addUsageValue(currentRecord[key], value); - } - return result; - } - - return next; -} - -export function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { - if (next == null) { - return current ?? null; - } - return addUsageValue(current, next) as ModelUsage; -} - -export function usageWithRequestCount(usage: ModelUsage): ModelUsage { - const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; - return { - ...usage, - total_reqs: totalReqs, - }; -} - -export function accumulateUsagePerModel( - current: Record | null | undefined, - model: string, - next: ModelUsage | null | undefined -): Record | null { - if (next == null) { - return current ?? null; - } - - const usagePerModel = { ...(current ?? {}) }; - const modelName = model.trim() || "unknown"; - usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; - return usagePerModel; -} - -export { getExtensionRoot } from "./prompt"; -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; -import * as crypto from "crypto"; -import matter from "gray-matter"; -import ejs from "ejs"; -import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; -import { launchNotifyScript } from "./common/notify"; -import { buildThinkingRequestOptions } from "./common/openai-thinking"; -import { supportsMultimodal } from "./common/model-capabilities"; -import { - getCompactPrompt, - getDefaultSkillPrompt, - getExtensionRoot, - getRuntimeContext, - getSystemPrompt, - getTools, - type ToolDefinition, -} from "./prompt"; -import { - type ProcessTimeoutControl, - type ProcessTimeoutInfo, - type ToolCallExecution, - type ToolExecutionHooks, - ToolExecutor, -} from "./tools/executor"; -import { McpManager } from "./mcp/mcp-manager"; - -import { logApiError } from "./common/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; -import { killProcessTree } from "./common/process-tree"; -import { GitFileHistory } from "./common/file-history"; -import { getSnippet } from "./common/state"; -import { - appendProjectPermissionAllows, - buildPermissionToolExecution, - computeToolCallPermissions, - hasUserPermissionReplies, - normalizeAskPermissions, - parseToolCallForPermissions, - type PermissionToolCall, -} from "./common/permissions"; - -const MAX_SESSION_ENTRIES = 50; -const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; -const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; - -type ChatCompletionDebugOptions = { - enabled?: boolean; - location: string; - baseURL?: string; - params?: Record; -}; export class SessionManager { private readonly projectRoot: string; From d65260e2419c85bb4bc78c58deb75d6f7eaeb828 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 17:03:23 +0800 Subject: [PATCH 095/212] =?UTF-8?q?refactor(session):=20=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=20session.ts=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/session.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/session.ts b/src/session.ts index a9fc39e8..876d2561 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2,7 +2,6 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; -import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "openai/resources/chat/completions"; @@ -12,6 +11,7 @@ import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilit import { getCompactPrompt, getDefaultSkillPrompt, + getExtensionRoot, getRuntimeContext, getSystemPrompt, getTools, @@ -135,15 +135,6 @@ function accumulateUsagePerModel( return usagePerModel; } -function getExtensionRoot(): string { - if (typeof __dirname !== "undefined") { - return path.resolve(__dirname, ".."); - } - - const currentFilePath = fileURLToPath(import.meta.url); - return path.resolve(path.dirname(currentFilePath), ".."); -} - function getTotalTokens(usage: ModelUsage | null | undefined): number { if (!isUsageRecord(usage)) { return 0; From e25cb9eca1ce5b6b07049003271bd4df400c9ffc Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 17:49:39 +0800 Subject: [PATCH 096/212] =?UTF-8?q?fix(ui):=20=E4=BC=98=E5=8C=96=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E5=AF=BC=E8=88=AA=E5=92=8C=E7=B2=98=E8=B4=B4=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在历史导航中同步捕获当前草稿,避免异步状态不一致 - 修正使用最新捕获草稿替代旧状态以设置缓冲区文本 - 在粘贴处理钩子中引入 useEffect 钩子以响应缓冲区文本变化 - 通过副作用保持衍生标志状态与缓冲区文本同步 - 移除潜在的异步更新问题,提升用户交互体验 --- src/ui/hooks/useHistoryNavigation.ts | 4 +++- src/ui/hooks/usePasteHandling.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ui/hooks/useHistoryNavigation.ts b/src/ui/hooks/useHistoryNavigation.ts index 433d493c..54ccabfd 100644 --- a/src/ui/hooks/useHistoryNavigation.ts +++ b/src/ui/hooks/useHistoryNavigation.ts @@ -38,13 +38,15 @@ export function useHistoryNavigation( const previousCursor = historyCursor === -1 ? promptHistory.length : historyCursor; const nextCursor = Math.max(0, Math.min(promptHistory.length, previousCursor + direction)); + // Capture the current draft synchronously before `setDraftBeforeHistory`. + const draft = historyCursor === -1 ? buffer.text : draftBeforeHistory; if (historyCursor === -1) { setDraftBeforeHistory(buffer.text); } if (nextCursor === promptHistory.length) { - const text = draftBeforeHistory ?? ""; + const text = draft ?? ""; setBuffer({ text, cursor: text.length }); setHistoryCursor(-1); setDraftBeforeHistory(null); diff --git a/src/ui/hooks/usePasteHandling.ts b/src/ui/hooks/usePasteHandling.ts index 50cae754..beaf0859 100644 --- a/src/ui/hooks/usePasteHandling.ts +++ b/src/ui/hooks/usePasteHandling.ts @@ -1,5 +1,5 @@ import type React from "react"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { PromptBufferState } from "../core/prompt-buffer"; import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../core/prompt-buffer"; @@ -51,6 +51,13 @@ export function usePasteHandling( setHasExpandedRegions(expandedRegionsRef.current.size > 0); } + // Recompute derived flags whenever the buffer text changes, so they stay + // in sync after any state update (e.g. large paste, expand/collapse, undo). + useEffect(() => { + refreshDerivedFlags(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [buffer.text]); + function handlePaste(pastedText: string): void { const totalChars = pastedText.length; From 64edb904d729ce066315ec05b4e27c422c1c9a47 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 21:13:32 +0800 Subject: [PATCH 097/212] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0=20ink?= =?UTF-8?q?=20=E4=BE=9D=E8=B5=96=E5=88=B0=207.0.4=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 升级 package.json 中 ink 版本号从 7.0.1 到 7.0.4 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index dfa3fbb3..ab845a3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "gradient-string": "^3.0.0", "gray-matter": "^4.0.3", "ignore": "^7.0.5", - "ink": "^7.0.1", + "ink": "^7.0.4", "ink-gradient": "^4.0.0", "openai": "^6.35.0", "react": "^19.2.5", @@ -2454,9 +2454,9 @@ } }, "node_modules/ink": { - "version": "7.0.1", - "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.1.tgz", - "integrity": "sha512-o6LAC268PLawlGVYrXTyaTfke4VtJftEheuwbgkQf7yvSXyWp1nRwBbAyKEkWXFZZsW/la5wrMuNbuBvZK2C1w==", + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.4.tgz", + "integrity": "sha512-4wsM/gMKOT2ZANNTJibI6I9IcwBfobqv/CgaDcwvOaCREZIQxo3iGQS7qPHa2hmA67NYltZWCMtBDELB/mcbJQ==", "license": "MIT", "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", diff --git a/package.json b/package.json index 71c171c8..1e93c667 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "gradient-string": "^3.0.0", "gray-matter": "^4.0.3", "ignore": "^7.0.5", - "ink": "^7.0.1", + "ink": "^7.0.4", "ink-gradient": "^4.0.0", "openai": "^6.35.0", "react": "^19.2.5", From f27c2c49bb656e28080b8703b30f2a0d3bc6448d Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 27 May 2026 10:14:01 +0800 Subject: [PATCH 098/212] feat: Edit tool parameter prioritize displaying the file path --- src/session.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/session.ts b/src/session.ts index 349c48e9..c5f34e7e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2547,6 +2547,12 @@ ${skillMd} return typeof args.explanation === "string" ? args.explanation.trim() : ""; } else if (toolName === "write") { return typeof args.file_path === "string" ? args.file_path.trim() : ""; + } else if (toolName === "edit") { + const filePath = typeof args.file_path === "string" ? args.file_path.trim() : ""; + if (filePath) { + return filePath; + } + return typeof args.snippet_id === "string" ? args.snippet_id.trim() : ""; } const firstKey = Object.keys(args)[0]; From c6f2abf2e815ffb725ef27657929e3a2b464d0c9 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 27 May 2026 14:18:29 +0800 Subject: [PATCH 099/212] feat(session): rebuild session state to support restoring snippet from history. --- src/common/state.ts | 313 ++++++++++++++++++++++++++++++++++++++ src/session.ts | 4 +- src/tests/session.test.ts | 77 ++++++++++ 3 files changed, 392 insertions(+), 2 deletions(-) diff --git a/src/common/state.ts b/src/common/state.ts index 29346680..46e0b52d 100644 --- a/src/common/state.ts +++ b/src/common/state.ts @@ -1,4 +1,6 @@ +import * as fs from "fs"; import * as path from "path"; +import { readTextFileWithMetadata } from "./file-utils"; import { posixPathToWindowsPath } from "./shell-utils"; export type FileLineEnding = "LF" | "CRLF"; @@ -25,6 +27,11 @@ export type FileSnippet = { scopeType: "snippet" | "full"; }; +export type SessionStateHistoryMessage = { + role?: unknown; + content?: unknown; +}; + const fileStatesBySession = new Map>(); const snippetsBySession = new Map>(); const snippetCountersBySession = new Map(); @@ -39,9 +46,24 @@ export function clearSessionState(sessionId: string): void { fileStatesBySession.delete(sessionId); snippetsBySession.delete(sessionId); snippetCountersBySession.delete(sessionId); + fullFileSnippetCountersBySession.delete(sessionId); fileVersionsBySession.delete(sessionId); } +export function hasSessionState(sessionId: string): boolean { + if (!sessionId) { + return false; + } + + return Boolean( + fileStatesBySession.get(sessionId)?.size || + snippetsBySession.get(sessionId)?.size || + snippetCountersBySession.has(sessionId) || + fullFileSnippetCountersBySession.has(sessionId) || + fileVersionsBySession.get(sessionId)?.size + ); +} + export function normalizeFilePath(filePath: string, platform: NodeJS.Platform = process.platform): string { const nativePath = normalizeNativeFilePath(filePath, platform); return platform === "win32" ? path.win32.normalize(nativePath) : path.normalize(nativePath); @@ -178,6 +200,32 @@ export function createFullFileSnippet( return createSnippetWithId(sessionId, filePath, startLine, endLine, preview, `full_file_${nextCounter}`, "full"); } +export function restoreSnippet( + sessionId: string, + snippet: { + id: string; + filePath: string; + startLine: number; + endLine: number; + preview?: string; + scopeType?: FileSnippet["scopeType"]; + } +): FileSnippet | null { + const restored = createSnippetWithId( + sessionId, + snippet.filePath, + snippet.startLine, + snippet.endLine, + snippet.preview ?? "", + snippet.id, + snippet.scopeType ?? inferSnippetScopeType(snippet.id) + ); + if (restored) { + updateSnippetCounters(sessionId, snippet.id); + } + return restored; +} + function createSnippetWithId( sessionId: string, filePath: string, @@ -210,6 +258,27 @@ function createSnippetWithId( return snippet; } +function inferSnippetScopeType(id: string): FileSnippet["scopeType"] { + return id.startsWith("full_file_") ? "full" : "snippet"; +} + +function updateSnippetCounters(sessionId: string, id: string): void { + const fullFileMatch = /^full_file_(\d+)$/.exec(id); + if (fullFileMatch) { + const nextCounter = Number(fullFileMatch[1]) + 1; + const current = fullFileSnippetCountersBySession.get(sessionId) ?? 0; + fullFileSnippetCountersBySession.set(sessionId, Math.max(current, nextCounter)); + return; + } + + const snippetMatch = /^snippet_(\d+)$/.exec(id); + if (snippetMatch) { + const currentCounter = Number(snippetMatch[1]); + const current = snippetCountersBySession.get(sessionId) ?? 0; + snippetCountersBySession.set(sessionId, Math.max(current, currentCounter)); + } +} + export function getSnippet(sessionId: string, snippetId: string): FileSnippet | null { if (!sessionId || !snippetId) { return null; @@ -220,3 +289,247 @@ export function getSnippet(sessionId: string, snippetId: string): FileSnippet | export function hasSnippetOutdatedFileVersion(sessionId: string, snippet: FileSnippet): boolean { return getFileVersion(sessionId, snippet.filePath) > snippet.fileVersion; } + +export function rebuildSessionStateFromHistory( + sessionId: string, + messages: Iterable +): void { + if (!sessionId || hasSessionState(sessionId)) { + return; + } + + for (const message of messages) { + if (message.role !== "tool" || typeof message.content !== "string") { + continue; + } + + const result = parsePersistedToolResult(message.content); + if (!result || result.ok !== true) { + continue; + } + + const metadata = asRecord(result.metadata); + if (!metadata) { + continue; + } + + if (result.name === "read") { + rebuildReadResult(sessionId, result, metadata); + } else if (result.name === "edit") { + rebuildEditResult(sessionId, metadata); + } else if (result.name === "write") { + rebuildWriteResult(sessionId, metadata); + } + } +} + +function rebuildReadResult( + sessionId: string, + result: Record, + metadata: Record +): void { + const snippet = asRecord(metadata.snippet); + if (!snippet) { + return; + } + + const restored = restoreSnippetFromRecord(sessionId, snippet, { + idKey: "id", + filePathKey: "filePath", + startLineKey: "startLine", + endLineKey: "endLine", + preview: typeof result.output === "string" ? result.output : "", + }); + if (!restored) { + return; + } + + refreshRebuiltFileState(sessionId, restored.filePath, { + scopeType: restored.scopeType, + startLine: restored.startLine, + endLine: restored.endLine, + incrementVersion: false, + }); +} + +function rebuildEditResult(sessionId: string, metadata: Record): void { + const scope = asRecord(metadata.scope); + if (scope) { + restoreSnippetFromRecord(sessionId, scope, { + idKey: "snippet_id", + filePathKey: "file_path", + startLineKey: "start_line", + endLineKey: "end_line", + scopeType: metadata.read_scope_type === "full" ? "full" : undefined, + }); + } + + const scopeFilePath = typeof scope?.file_path === "string" ? scope.file_path : undefined; + rebuildCandidateSnippets(sessionId, metadata, scopeFilePath); + + const filePath = typeof metadata.file_path === "string" ? metadata.file_path : scopeFilePath; + if (filePath && metadata.cache_refreshed === true) { + refreshRebuiltFileState(sessionId, filePath, { incrementVersion: true }); + } +} + +function rebuildWriteResult(sessionId: string, metadata: Record): void { + if (metadata.cache_refreshed !== true || typeof metadata.file_path !== "string") { + return; + } + + refreshRebuiltFileState(sessionId, metadata.file_path, { incrementVersion: true }); +} + +function rebuildCandidateSnippets( + sessionId: string, + metadata: Record, + filePath: string | undefined +): void { + if (!filePath) { + return; + } + + const candidates = Array.isArray(metadata.candidates) ? metadata.candidates : []; + for (const candidate of candidates) { + const record = asRecord(candidate); + if (!record) { + continue; + } + restoreSnippetFromRecord( + sessionId, + { ...record, file_path: filePath }, + { + idKey: "snippet_id", + filePathKey: "file_path", + startLineKey: "start_line", + endLineKey: "end_line", + scopeType: "snippet", + preview: typeof record.preview === "string" ? record.preview : "", + } + ); + } + + const closestMatch = asRecord(metadata.closest_match); + if (closestMatch) { + restoreSnippetFromRecord( + sessionId, + { ...closestMatch, file_path: filePath }, + { + idKey: "snippet_id", + filePathKey: "file_path", + startLineKey: "start_line", + endLineKey: "end_line", + scopeType: "snippet", + preview: typeof closestMatch.preview === "string" ? closestMatch.preview : "", + } + ); + } +} + +function restoreSnippetFromRecord( + sessionId: string, + record: Record, + options: { + idKey: string; + filePathKey: string; + startLineKey: string; + endLineKey: string; + preview?: string; + scopeType?: FileSnippet["scopeType"]; + } +): FileSnippet | null { + const rawId = record[options.idKey]; + const rawFilePath = record[options.filePathKey]; + const id = typeof rawId === "string" ? rawId.trim() : ""; + const filePath = typeof rawFilePath === "string" ? normalizeFilePath(rawFilePath) : ""; + const startLine = toPositiveInteger(record[options.startLineKey]); + const endLine = toPositiveInteger(record[options.endLineKey]); + if (!id || !filePath || startLine === null || endLine === null) { + return null; + } + + return restoreSnippet(sessionId, { + id, + filePath, + startLine, + endLine, + preview: options.preview, + scopeType: options.scopeType, + }); +} + +function refreshRebuiltFileState( + sessionId: string, + rawFilePath: string, + options: { + scopeType?: FileSnippet["scopeType"]; + startLine?: number; + endLine?: number; + incrementVersion?: boolean; + } = {} +): void { + const filePath = normalizeFilePath(rawFilePath); + if (!filePath || !fs.existsSync(filePath)) { + return; + } + + try { + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + return; + } + + const metadata = readTextFileWithMetadata(filePath); + const isPartialView = options.scopeType === "snippet"; + const content = isPartialView + ? metadata.content + .split("\n") + .slice((options.startLine ?? 1) - 1, options.endLine) + .join("\n") + : metadata.content; + + recordFileState( + sessionId, + { + filePath, + content, + timestamp: metadata.timestamp, + offset: isPartialView ? options.startLine : undefined, + limit: + isPartialView && options.startLine !== undefined && options.endLine !== undefined + ? Math.max(1, options.endLine - options.startLine + 1) + : undefined, + isPartialView, + encoding: metadata.encoding, + lineEndings: metadata.lineEndings, + }, + { incrementVersion: options.incrementVersion } + ); + } catch { + // Best-effort restore: later tool execution will return the precise filesystem error. + } +} + +function parsePersistedToolResult(content: string): Record | null { + try { + return asRecord(JSON.parse(content)); + } catch { + return null; + } +} + +function asRecord(value: unknown): Record | null { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return null; + } + return value as Record; +} + +function toPositiveInteger(value: unknown): number | null { + const numberValue = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; + if (!Number.isInteger(numberValue) || numberValue < 1) { + return null; + } + return numberValue; +} diff --git a/src/session.ts b/src/session.ts index c5f34e7e..37a7dc11 100644 --- a/src/session.ts +++ b/src/session.ts @@ -31,7 +31,7 @@ import { logApiError } from "./common/error-logger"; import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; import { killProcessTree } from "./common/process-tree"; import { GitFileHistory } from "./common/file-history"; -import { getSnippet } from "./common/state"; +import { clearSessionState, getSnippet, rebuildSessionStateFromHistory } from "./common/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, @@ -45,7 +45,6 @@ import { type UserToolPermission, } from "./common/permissions"; import { clearSessionWorkingDir } from "./tools/bash-handler"; -import { clearSessionState } from "./common/state"; export type { PermissionScope } from "./settings"; export type { @@ -1143,6 +1142,7 @@ ${skillMd} const { client, model, baseURL, thinkingEnabled, reasoningEffort, debugLogEnabled, notify, env } = this.createOpenAIClient(); const now = new Date().toISOString(); + rebuildSessionStateFromHistory(sessionId, this.listSessionMessages(sessionId)); if (!client) { this.updateSessionEntry(sessionId, (entry) => ({ diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 6af3cb2d..ff6dc926 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -5,6 +5,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/file-history"; +import { clearSessionState } from "../common/state"; import { type SessionMessage } from "../session"; import { SessionManager } from "../session"; @@ -1257,6 +1258,75 @@ test("replySession /continue runs trailing pending tool calls before requesting ); }); +test("replySession rebuilds snippet state from persisted read history before editing", async () => { + const workspace = createTempDir("deepcode-rebuild-snippet-workspace-"); + const home = createTempDir("deepcode-rebuild-snippet-home-"); + setHomeDir(home); + + const filePath = path.join(workspace, "note.txt"); + fs.writeFileSync(filePath, "alpha\nbeta\n", "utf8"); + + const responses = [ + createToolCallResponse( + [ + { + id: "call-edit", + type: "function", + function: { + name: "edit", + arguments: JSON.stringify({ + snippet_id: "full_file_5", + file_path: filePath, + old_string: "beta", + new_string: "gamma", + }), + }, + }, + ], + { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } + ), + createChatResponse("done", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }), + ]; + const manager = createMockedClientSessionManager(workspace, responses); + const originalActivateSession = manager.activateSession.bind(manager); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const readToolMessage = (manager as any).buildToolMessage( + sessionId, + "call-read", + JSON.stringify({ + ok: true, + name: "read", + output: " 1\talpha\n 2\tbeta\n", + metadata: { + snippet: { + id: "full_file_5", + filePath, + startLine: 1, + endLine: 3, + }, + }, + }), + { name: "read", arguments: JSON.stringify({ file_path: filePath }) } + ) as SessionMessage; + (manager as any).appendSessionMessage(sessionId, readToolMessage); + + clearSessionState(sessionId); + (manager as any).activateSession = originalActivateSession; + + await manager.replySession(sessionId, { text: "change beta" }); + + assert.equal(fs.readFileSync(filePath, "utf8"), "alpha\ngamma\n"); + const editToolMessage = manager.listSessionMessages(sessionId).find((message) => { + const params = message.messageParams as { tool_call_id?: string } | null; + return message.role === "tool" && params?.tool_call_id === "call-edit"; + }); + assert.ok(editToolMessage); + assert.match(editToolMessage.content ?? "", /"ok":true|"ok": true/); + assert.doesNotMatch(editToolMessage.content ?? "", /Unknown snippet_id/); +}); + test("activateSession pauses for permission when a tool call requires ask", async () => { const workspace = createTempDir("deepcode-permission-ask-workspace-"); const home = createTempDir("deepcode-permission-ask-home-"); @@ -2712,6 +2782,13 @@ function createChatResponse(content: string, usage: Record): un }; } +function createToolCallResponse(toolCalls: unknown[], usage: Record): unknown { + return { + choices: [{ message: { content: "", tool_calls: toolCalls } }], + usage, + }; +} + function buildTestMessage( id: string, sessionId: string, From 45c0d958c3d94aad1870f55d36048c7b91502f01 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 27 May 2026 14:39:25 +0800 Subject: [PATCH 100/212] feat(file-mentions): increase default max items and add noisy directory ignores --- src/tests/file-mentions.test.ts | 50 +++++++++++++++++++++++++++++++-- src/ui/core/file-mentions.ts | 44 +++++++++++++++++++++++++++-- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/tests/file-mentions.test.ts b/src/tests/file-mentions.test.ts index 93d9bccc..36d2c21d 100644 --- a/src/tests/file-mentions.test.ts +++ b/src/tests/file-mentions.test.ts @@ -86,18 +86,62 @@ test("scanFileMentionItems returns relative slash-separated files and directorie try { fs.mkdirSync(path.join(root, "src")); fs.writeFileSync(path.join(root, "src", "index.ts"), ""); - fs.mkdirSync(path.join(root, "node_modules")); - fs.writeFileSync(path.join(root, "node_modules", "ignored.js"), ""); + fs.mkdirSync(path.join(root, "vendor")); + fs.writeFileSync(path.join(root, "vendor", "dep.js"), ""); assert.deepEqual( scanFileMentionItems(root).map((item) => item.path), - ["node_modules/", "node_modules/ignored.js", "src/", "src/index.ts"] + ["src/", "src/index.ts", "vendor/", "vendor/dep.js"] ); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); +test("scanFileMentionItems applies default noisy-directory ignores when no gitignore is applicable", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-")); + try { + for (const directory of [ + ".next", + ".pytest_cache", + ".ruff_cache", + "__pycache__", + "build", + "dist", + "node_modules", + "out", + "target", + ]) { + fs.mkdirSync(path.join(root, directory)); + fs.writeFileSync(path.join(root, directory, "ignored.txt"), ""); + } + fs.mkdirSync(path.join(root, ".config")); + fs.writeFileSync(path.join(root, ".config", "settings.json"), ""); + fs.mkdirSync(path.join(root, "src")); + fs.writeFileSync(path.join(root, "src", "index.ts"), ""); + + assert.deepEqual( + scanFileMentionItems(root).map((item) => item.path), + [".config/", ".config/settings.json", "src/", "src/index.ts"] + ); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("scanFileMentionItems default max item cap is above 2000", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-")); + try { + for (let index = 0; index < 2001; index++) { + fs.writeFileSync(path.join(root, `file-${index.toString().padStart(4, "0")}.txt`), ""); + } + + assert.equal(scanFileMentionItems(root).length, 2001); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + test("scanFileMentionItems respects project gitignore patterns inside git repositories", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-file-mentions-")); try { diff --git a/src/ui/core/file-mentions.ts b/src/ui/core/file-mentions.ts index ae9c8b99..54847f12 100644 --- a/src/ui/core/file-mentions.ts +++ b/src/ui/core/file-mentions.ts @@ -15,8 +15,20 @@ export type FileMentionToken = { quoted: boolean; }; -const DEFAULT_MAX_ITEMS = 2000; +const DEFAULT_MAX_ITEMS = 20000; const DEFAULT_MAX_DEPTH = 8; +const DEFAULT_NOISY_DIR_NAMES = [ + ".git", + ".next", + ".pytest_cache", + ".ruff_cache", + "__pycache__", + "build", + "dist", + "node_modules", + "out", + "target", +]; type IgnoreMatcher = { base: string; @@ -104,7 +116,8 @@ export function scanFileMentionItems(root: string, maxItems = DEFAULT_MAX_ITEMS) if (rootRealPath) { visitedDirectories.add(rootRealPath); } - visit(root, 0, loadAncestorIgnoreMatchers(root, gitRoot)); + const initialMatchers = [...loadDefaultIgnoreMatchers(root, gitRoot), ...loadAncestorIgnoreMatchers(root, gitRoot)]; + visit(root, 0, initialMatchers); return items; } @@ -162,6 +175,33 @@ function loadDirectoryIgnoreMatchers(directory: string, gitRoot: string | null): return matchers; } +function loadDefaultIgnoreMatchers(root: string, gitRoot: string | null): IgnoreMatcher[] { + if (hasApplicableGitignore(root, gitRoot)) { + return []; + } + const patterns = DEFAULT_NOISY_DIR_NAMES.map((name) => `${name}/`); + return [{ base: root, matcher: ignore().add(patterns) }]; +} + +function hasApplicableGitignore(root: string, gitRoot: string | null): boolean { + if (!gitRoot) { + return false; + } + + const resolvedGitRoot = path.resolve(gitRoot); + let current = path.resolve(root); + while (isPathInsideOrEqual(current, resolvedGitRoot)) { + if (fs.existsSync(path.join(current, ".gitignore"))) { + return true; + } + if (current === resolvedGitRoot) { + break; + } + current = path.dirname(current); + } + return false; +} + function loadAncestorIgnoreMatchers(root: string, gitRoot: string | null): IgnoreMatcher[] { const resolvedRoot = path.resolve(root); const ancestors: string[] = []; From bb652cac75e3ad4cd33b54ae246aff19865b6592 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 28 May 2026 14:11:14 +0800 Subject: [PATCH 101/212] feat: update AGENTS.md and init_command.md.ejs --- .deepcode/AGENTS.md | 50 ++++++++++++++++----------- templates/prompts/init_command.md.ejs | 2 +- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/.deepcode/AGENTS.md b/.deepcode/AGENTS.md index 7f9cf35d..bdd787ab 100644 --- a/.deepcode/AGENTS.md +++ b/.deepcode/AGENTS.md @@ -9,24 +9,21 @@ src/ ├── settings.ts # Settings resolution from ~/.deepcode/settings.json ├── prompt.ts # System prompt builder, tool definitions, built-in skills ├── common/ +│ ├── bash-timeout.ts # Bash command timeout enforcement +│ ├── debug-logger.ts # Debug logging for OpenAI API calls +│ ├── error-logger.ts # API error logging +│ ├── file-history.ts # GitFileHistory — checkpoint/undo via Git branches +│ ├── file-utils.ts # File read/write with encoding and diff preview │ ├── model-capabilities.ts # Model detection and thinking-mode defaults +│ ├── notify.ts # Desktop notification after LLM turn completion +│ ├── openai-client.ts # OpenAI client singleton with keep-alive agent │ ├── openai-thinking.ts # OpenAI thinking request options builder -│ ├── file-utils.ts # File read/write with encoding and diff preview +│ ├── permissions.ts # Permission scoping, decisions, and tool-call tracking +│ ├── process-tree.ts # Process tree construction and orphan detection │ ├── shell-utils.ts # Shell path resolution (Git Bash, zsh, bash) │ ├── state.ts # In-memory file state and snippet tracking -│ ├── runtime.ts # Tool validation runtime helpers -│ ├── notify.ts # Desktop notification after LLM turn completion -│ ├── debug-logger.ts # Debug logging for OpenAI API calls -│ └── error-logger.ts # API error logging -├── ui/ -│ ├── App.tsx # Root Ink component — state, routing, session orchestration -│ ├── PromptInput.tsx # Multi-line input with file mentions (@), slash commands, image paste, skills -│ ├── MessageView.tsx # Renders assistant/tool messages with markdown -│ ├── McpStatusList.tsx # MCP server connection status and available tools -│ ├── ProcessStdoutView.tsx # Ctrl+O fullscreen overlay for live process stdout -│ ├── UpdatePrompt.tsx # UpdatePlan task list progress display -│ ├── fileMentions.ts # @-mention file scanning, filtering, and insertion -│ └── ... +│ ├── update-check.ts # Latest-version check against npm registry +│ └── validate.ts # Tool validation runtime helpers (was runtime.ts) ├── mcp/ │ ├── mcp-client.ts # MCP client — JSON-RPC communication with MCP servers │ └── mcp-manager.ts # MCP manager — lifecycle, tool registration, execution, status @@ -39,12 +36,21 @@ src/ │ ├── update-plan-handler.ts # Updates the task plan progress display │ ├── web-search-handler.ts # Web search via natural language queries │ └── ask-user-question-handler.ts # Interactive user prompts with options +├── ui/ +│ ├── components/ # Reusable Ink components (MessageView, DropdownMenu, ModelsDropdown, SkillsDropdown, etc.) +│ ├── contexts/ # React contexts (AppContext, RawModeContext) +│ ├── hooks/ # Custom hooks (cursor, useHistoryNavigation, usePasteHandling, useTerminalInput) +│ ├── views/ # Top-level screen components (App, PromptInput, SessionList, PermissionPrompt, ProcessStdoutView, etc.) +│ ├── core/ # Core UI logic (file-mentions, slash-commands, prompt-buffer, thinking-state, etc.) +│ ├── utils/ # Shared utility helpers +│ ├── index.ts # UI module barrel exports +│ └── constants.ts # UI-wide constants ├── tests/ # One *.test.ts per source module, plus run-tests.mjs templates/ ├── tools/ # Tool descriptions fed to the LLM ├── skills/ # Built-in skill definitions (agent-drift-guard, plan-and-execute) -├── prompts/ # EJS templates (e.g., init_command.md.ejs) -docs/ # User-facing documentation (configuration, MCP, skills) +└── prompts/ # EJS templates (e.g., init_command.md.ejs) +docs/ # User-facing documentation (configuration, MCP, notify, permissions) dist/ # Bundled CLI output (gitignored) ``` @@ -78,13 +84,13 @@ Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundl **Formatting/Linting**: Prettier + ESLint (typescript-eslint, react-hooks). Run `npm run check` before pushing. On commit, Husky + lint-staged auto-formats staged `*.{ts,tsx,js,mjs,cjs,ejs,jsx}` and `*.json` files. -**File naming**: `kebab-case.ts` for modules, `kebab-case.tsx` for React/Ink components. Test files: `*.test.ts`. +**File naming**: `kebab-case.ts` for modules, `kebab-case.tsx` for React/Ink components. Test files: `*.test.ts` (always kebab-case). ## Testing Guidelines - **Framework**: Node.js native test runner (`node:test`) with `tsx` for TypeScript - **Assertions**: `node:assert/strict` -- **Coverage**: Target meaningful unit tests for core logic (session management, tool handlers, settings resolution, prompt buffer). Test files are in `src/tests/` matching the source module name. +- **Coverage**: Target meaningful unit tests for core logic (session management, tool handlers, settings resolution, prompt buffer, permissions, MCP client). Test files are in `src/tests/` matching the source module name. - **Test naming**: `describe`/`test` blocks with descriptive names. Example: `test("SessionManager preserves structured system content when building OpenAI messages", ...)` - **Relaxed lint rules**: Test files allow `any` and unused vars. - Run all tests with `npm test` before submitting a PR. A cross-platform test runner is available at `src/tests/run-tests.mjs`. @@ -109,13 +115,17 @@ Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundl ## Architecture Overview -The CLI (`@vegamo/deepcode-cli`) renders a terminal UI using [Ink](https://github.com/vadimdemedes/ink) (React for terminals). `SessionManager` drives the LLM interaction loop: it builds system prompts, sends user messages with optional skills/images, streams responses, executes tool calls via `ToolExecutor`, and compacts context when token thresholds are exceeded (512K for DeepSeek V4 models, 128K for others). +The CLI (`@vegamo/deepcode-cli`) renders a terminal UI using [Ink](https://github.com/vadimdemedes/ink) (React for terminals). `SessionManager` drives the LLM interaction loop: it builds system prompts, sends user messages with optional skills/images, streams responses, executes tool calls via `ToolExecutor`, and compacts context when token thresholds are exceeded (512K for DeepSeek V4 models, 128K for others). OpenAI client connectivity is managed by `createOpenAIClient()` in `src/common/openai-client.ts`, which caches the client singleton and applies a 180-second keep-alive timeout. Seven built-in tools are available to the LLM: `bash`, `read`, `write`, `edit`, `AskUserQuestion`, `UpdatePlan`, and `WebSearch`. Tool definitions are registered in `src/tools/executor.ts` and described to the LLM via `src/prompt.ts` and `templates/tools/`. The `UpdatePlan` tool enables the LLM to display and update a structured task list in the terminal. +A **permission system** (`src/common/permissions.ts`) controls tool execution scopes (read/write/delete/network/git-log, etc.) with configurable allow/deny/ask decisions. The `PermissionPrompt` view presents interactive prompts when a tool requires user approval. + +A **file history system** (`src/common/file-history.ts`) provides undo/checkpoint support by creating lightweight Git branches per session. The `GitFileHistory` class manages blob storage and branch references via a `.deepcode-file-history.json` manifest. + **Slash commands**: `/model`, `/new`, `/init`, `/resume`, `/continue`, `/mcp`, `/exit`, plus dynamic `/skill-name` for each loaded skill. -**Key UI features**: `@` file mentions in the prompt input (scans project files), `Ctrl+O` to view live process stdout in fullscreen, `Ctrl+V` to paste images, MCP server status display. +**Key UI features**: `@` file mentions in the prompt input (scans project files), `Ctrl+O` to view live process stdout in fullscreen, `Ctrl+V` to paste images, MCP server status display, undo selector, and permission prompts. **CLI flags**: `-p ` / `--prompt` to auto-submit a prompt on launch, `-v` / `--version`, `-h` / `--help`. diff --git a/templates/prompts/init_command.md.ejs b/templates/prompts/init_command.md.ejs index ec2f2f69..c8a22ce7 100644 --- a/templates/prompts/init_command.md.ejs +++ b/templates/prompts/init_command.md.ejs @@ -1,7 +1,7 @@ <% if (agentsMdFile == null) { %> Generate a file named ./AGENTS.md that serves as a contributor guide for this repository. <% } else { %> -Update <%= agentsMdFile %> that serves as a contributor guide for this repository. +Update <%= agentsMdFile %> to align it with repository changes made after the last time <%= agentsMdFile %> was modified. <% } %> Your goal is to produce a clear, concise, and well-structured document with descriptive headings and actionable explanations for each section. Follow the outline below, but adapt as needed — add sections if relevant, and omit those that do not apply to this project. From 084c84e46a28c3d5ec40b05a90e931c549c85c53 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 28 May 2026 16:32:57 +0800 Subject: [PATCH 102/212] feat: enhance file history management and add user prompt checkpointing --- src/common/file-history.ts | 82 +++++++++++++++++-- src/session.ts | 6 ++ src/tests/session.test.ts | 161 +++++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 8 deletions(-) diff --git a/src/common/file-history.ts b/src/common/file-history.ts index 2a41d9a1..b4a73046 100644 --- a/src/common/file-history.ts +++ b/src/common/file-history.ts @@ -9,12 +9,12 @@ const MANIFEST_PATH = ".deepcode-file-history.json"; type FileHistoryEntry = { path: string; - blob: string; + blob: string | null; mode: "100644"; }; type FileHistoryManifest = { - version: 1; + version: 1 | 2; files: Record; }; @@ -85,7 +85,11 @@ export class GitFileHistory { for (const filePath of absolutePaths) { const key = this.getFileKey(filePath); if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { - delete manifest.files[key]; + manifest.files[key] = { + path: filePath, + blob: null, + mode: "100644", + }; continue; } @@ -110,6 +114,24 @@ export class GitFileHistory { } } + recordTrackedFilesCheckpoint(sessionId: string, message: string): string | undefined { + const currentHash = this.ensureSession(sessionId); + if (!currentHash) { + return undefined; + } + + try { + const manifest = this.readManifest(currentHash); + const trackedPaths = Object.values(manifest.files).map((entry) => entry.path); + if (trackedPaths.length === 0) { + return currentHash; + } + return this.recordCheckpoint(sessionId, trackedPaths, message); + } catch { + return undefined; + } + } + canRestore(sessionId: string, checkpointHash: string): boolean { if (!isCommitHash(checkpointHash)) { return false; @@ -146,11 +168,15 @@ export class GitFileHistory { for (const [key, entry] of Object.entries(currentManifest.files)) { if (!targetManifest.files[key]) { - removeTrackedFile(entry.path); + this.restoreFirstKnownEntry(currentHash, key, entry.path); } } for (const entry of Object.values(targetManifest.files)) { + if (!entry.blob) { + removeTrackedFile(entry.path); + continue; + } fs.mkdirSync(path.dirname(entry.path), { recursive: true }); fs.writeFileSync(entry.path, this.readBlob(entry.blob)); } @@ -158,6 +184,33 @@ export class GitFileHistory { this.runGit(["update-ref", branchRef, checkpointHash]); } + private restoreFirstKnownEntry(currentHash: string | undefined, key: string, fallbackPath: string): void { + const firstEntry = currentHash ? this.findFirstKnownEntry(currentHash, key) : undefined; + const entry = firstEntry ?? { path: fallbackPath, blob: null, mode: "100644" as const }; + if (!entry.blob) { + removeTrackedFile(entry.path); + return; + } + + fs.mkdirSync(path.dirname(entry.path), { recursive: true }); + fs.writeFileSync(entry.path, this.readBlob(entry.blob)); + } + + private findFirstKnownEntry(currentHash: string, key: string): FileHistoryEntry | undefined { + const commitHashes = this.runGit(["rev-list", "--reverse", currentHash]) + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(isCommitHash); + + for (const commitHash of commitHashes) { + const entry = this.readManifest(commitHash).files[key]; + if (entry) { + return entry; + } + } + return undefined; + } + private getSessionBranchRef(sessionId: string): string | null { if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) { return null; @@ -182,6 +235,9 @@ export class GitFileHistory { const entries: string[] = [`100644 blob ${manifestBlob}\t${MANIFEST_PATH}\0`]; for (const [key, entry] of Object.entries(normalizedManifest.files)) { + if (!entry.blob) { + continue; + } entries.push(`${entry.mode} blob ${entry.blob}\t${key}\0`); } @@ -191,7 +247,12 @@ export class GitFileHistory { private readManifest(commitHash: string): FileHistoryManifest { const buffer = this.runGitBuffer(["cat-file", "blob", `${commitHash}:${MANIFEST_PATH}`]); const parsed = JSON.parse(buffer.toString("utf8")) as FileHistoryManifest; - if (!parsed || parsed.version !== 1 || !parsed.files || typeof parsed.files !== "object") { + if ( + !parsed || + (parsed.version !== 1 && parsed.version !== 2) || + !parsed.files || + typeof parsed.files !== "object" + ) { throw new Error("Invalid file history manifest."); } return normalizeManifest(parsed); @@ -256,13 +317,18 @@ export class GitFileHistory { } function emptyManifest(): FileHistoryManifest { - return { version: 1, files: {} }; + return { version: 2, files: {} }; } function normalizeManifest(manifest: FileHistoryManifest): FileHistoryManifest { const files: Record = {}; for (const [key, entry] of Object.entries(manifest.files).sort(([left], [right]) => left.localeCompare(right))) { - if (!isValidStoredPath(key) || !entry || entry.mode !== "100644" || !isCommitHash(entry.blob)) { + if ( + !isValidStoredPath(key) || + !entry || + entry.mode !== "100644" || + (entry.blob !== null && !isCommitHash(entry.blob)) + ) { throw new Error("Invalid file history manifest."); } files[key] = { @@ -271,7 +337,7 @@ function normalizeManifest(manifest: FileHistoryManifest): FileHistoryManifest { mode: "100644", }; } - return { version: 1, files }; + return { version: 2, files }; } function uniqueAbsolutePaths(filePaths: string[]): string[] { diff --git a/src/session.ts b/src/session.ts index 37a7dc11..fed4db5b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1013,6 +1013,7 @@ The candidate skills are as follows:\n\n`; this.appendSessionMessage(sessionId, instructionsMessage); } + this.recordUserPromptCheckpoint(sessionId); const userMessage = this.buildUserMessage(sessionId, userPrompt); this.appendSessionMessage(sessionId, userMessage); @@ -1087,6 +1088,7 @@ ${skillMd} this.reportNewPrompt(); this.ensureFileHistorySession(sessionId); + this.recordUserPromptCheckpoint(sessionId); const userMessage = this.buildUserMessage(sessionId, userPrompt); this.appendSessionMessage(sessionId, userMessage); @@ -1752,6 +1754,10 @@ ${skillMd} return this.getFileHistory().getCurrentCheckpointHash(sessionId); } + private recordUserPromptCheckpoint(sessionId: string): string | undefined { + return this.getFileHistory().recordTrackedFilesCheckpoint(sessionId, "User prompt checkpoint"); + } + private prepareFileMutationCheckpoint(sessionId: string, filePath: string): void { const fileHistory = this.getFileHistory(); const previousHash = fileHistory.ensureSession(sessionId); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index ff6dc926..0de25524 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -999,6 +999,68 @@ test("createSession initializes file-history repo and session branch", async (t) ); }); +test("createSession initializes an empty file-history manifest without scanning existing files", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-file-history-empty-init-workspace-"); + const home = createTempDir("deepcode-file-history-empty-init-home-"); + setHomeDir(home); + fs.writeFileSync(path.join(workspace, "unrelated.txt"), "keep me\n", "utf8"); + fs.mkdirSync(path.join(workspace, "nested")); + fs.writeFileSync(path.join(workspace, "nested", "another.txt"), "also keep me\n", "utf8"); + + const manager = createSessionManager(workspace, "machine-id-file-history-empty-init"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const userMessage = manager.listSessionMessages(sessionId).find((message) => message.role === "user"); + assert.ok(userMessage?.checkpointHash); + + const manifest = readFileHistoryManifest(home, workspace, userMessage.checkpointHash); + assert.deepEqual(manifest.files, {}); +}); + +test("replySession snapshots manual edits to tracked files before appending the user prompt", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-prompt-checkpoint-manual-edit-workspace-"); + const home = createTempDir("deepcode-prompt-checkpoint-manual-edit-home-"); + setHomeDir(home); + + const filePath = path.join(workspace, "hello_world.py"); + const manager = createSessionManager(workspace, "machine-id-prompt-checkpoint-manual-edit"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "create hello world" }); + const gitDir = getFileHistoryGitDir(home, workspace); + const fileHistory = new GitFileHistory(workspace, gitDir); + + fs.writeFileSync(filePath, 'print("Hello, World!")\n', "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [filePath], "created hello world")); + + const manualEdit = 'if name == main:\n print("Hello, World!")\n'; + fs.writeFileSync(filePath, manualEdit, "utf8"); + await manager.replySession(sessionId, { text: "I manually edited @hello_world.py, note it" }); + const manualEditUserMessage = manager + .listSessionMessages(sessionId) + .filter((message) => message.role === "user") + .at(-1); + assert.ok(manualEditUserMessage?.checkpointHash); + + fs.writeFileSync(filePath, 'if __name__ == "__main__":\n print("Hello, World!")\n', "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [filePath], "fixed hello world")); + + manager.restoreSessionCode(sessionId, manualEditUserMessage.id); + + assert.equal(fs.readFileSync(filePath, "utf8"), manualEdit); +}); + test("Write tool advances file-history while preserving the user prompt checkpoint", async (t) => { if (!hasGit()) { t.skip("git is not available"); @@ -1191,6 +1253,8 @@ test("restoreSessionCode restores project files from the recorded Git checkpoint const manager = createSessionManager(workspace, "machine-id-undo-code"); const sessionId = "session-code-restore"; const checkpointHash = createFileHistoryCommit(home, workspace, sessionId, { "tracked.txt": "before\n" }); + const fileHistory = new GitFileHistory(workspace, getFileHistoryGitDir(home, workspace)); + assert.ok(fileHistory.recordCheckpoint(sessionId, [path.join(workspace, "new.txt")], "pre-create new.txt")); createFileHistoryCommit(home, workspace, sessionId, { "tracked.txt": "after\n", "new.txt": "remove me\n" }); fs.writeFileSync(path.join(workspace, "tracked.txt"), "after\n", "utf8"); fs.writeFileSync(path.join(workspace, "new.txt"), "remove me\n", "utf8"); @@ -1206,6 +1270,91 @@ test("restoreSessionCode restores project files from the recorded Git checkpoint assert.equal(fs.existsSync(path.join(workspace, "new.txt")), false); }); +test("restoreSessionCode preserves files that predate their first tracked mutation", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-undo-preexisting-files-workspace-"); + const home = createTempDir("deepcode-undo-preexisting-files-home-"); + setHomeDir(home); + + const readmePath = path.join(workspace, "README.md"); + const readmeEnPath = path.join(workspace, "README-en.md"); + const readmeZhPath = path.join(workspace, "README-zh_CN.md"); + fs.writeFileSync(readmePath, "这是一个hello world演示项目\n", "utf8"); + fs.writeFileSync(readmeEnPath, "This is a hello world demo project.\n", "utf8"); + fs.writeFileSync(readmeZhPath, "", "utf8"); + + const manager = createSessionManager(workspace, "machine-id-undo-preexisting-files"); + const sessionId = "session-undo-preexisting-files"; + const gitDir = getFileHistoryGitDir(home, workspace); + const fileHistory = new GitFileHistory(workspace, gitDir); + fileHistory.ensureSession(sessionId); + + const targetCheckpoint = fileHistory.recordCheckpoint( + sessionId, + [readmePath, readmeEnPath], + "checkpoint before syncing all readmes" + ); + assert.ok(targetCheckpoint); + + assert.ok(fileHistory.recordCheckpoint(sessionId, [readmeZhPath], "pre-sync zh readme")); + fs.writeFileSync(readmePath, "Synced readme\n", "utf8"); + fs.writeFileSync(readmeEnPath, "Synced readme\n", "utf8"); + fs.writeFileSync(readmeZhPath, "Synced readme\n", "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [readmePath, readmeEnPath, readmeZhPath], "synced readmes")); + + (manager as any).appendSessionMessage(sessionId, { + ...buildTestMessage("user-with-readme-checkpoint", sessionId, "user", "sync README*.md"), + checkpointHash: targetCheckpoint, + }); + + manager.restoreSessionCode(sessionId, "user-with-readme-checkpoint"); + + assert.equal(fs.readFileSync(readmePath, "utf8"), "这是一个hello world演示项目\n"); + assert.equal(fs.readFileSync(readmeEnPath, "utf8"), "This is a hello world demo project.\n"); + assert.equal(fs.readFileSync(readmeZhPath, "utf8"), ""); +}); + +test("restoreSessionCode restores deleted tracked files and leaves unrelated files alone", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-undo-deleted-files-workspace-"); + const home = createTempDir("deepcode-undo-deleted-files-home-"); + setHomeDir(home); + + const trackedPath = path.join(workspace, "tracked.txt"); + const unrelatedPath = path.join(workspace, "unrelated.txt"); + fs.writeFileSync(trackedPath, "before delete\n", "utf8"); + fs.writeFileSync(unrelatedPath, "do not touch\n", "utf8"); + + const manager = createSessionManager(workspace, "machine-id-undo-deleted-files"); + const sessionId = "session-undo-deleted-files"; + const gitDir = getFileHistoryGitDir(home, workspace); + const fileHistory = new GitFileHistory(workspace, gitDir); + fileHistory.ensureSession(sessionId); + const targetCheckpoint = fileHistory.recordCheckpoint(sessionId, [trackedPath], "before delete"); + assert.ok(targetCheckpoint); + + fs.unlinkSync(trackedPath); + assert.ok(fileHistory.recordCheckpoint(sessionId, [trackedPath], "after delete")); + + (manager as any).appendSessionMessage(sessionId, { + ...buildTestMessage("user-before-delete", sessionId, "user", "restore deleted file"), + checkpointHash: targetCheckpoint, + }); + + manager.restoreSessionCode(sessionId, "user-before-delete"); + + assert.equal(fs.readFileSync(trackedPath, "utf8"), "before delete\n"); + assert.equal(fs.readFileSync(unrelatedPath, "utf8"), "do not touch\n"); +}); + test("replySession /continue runs trailing pending tool calls before requesting another response", async () => { const workspace = createTempDir("deepcode-continue-tool-workspace-"); const home = createTempDir("deepcode-continue-tool-home-"); @@ -2617,6 +2766,18 @@ function createFileHistoryCommit( return commitHash; } +function getFileHistoryGitDir(home: string, workspace: string): string { + const projectCode = workspace.replace(/[\\/]/g, "-").replace(/:/g, ""); + return path.join(home, ".deepcode", "projects", projectCode, "file-history", ".git"); +} + +function readFileHistoryManifest(home: string, workspace: string, checkpointHash: string): any { + const gitDir = getFileHistoryGitDir(home, workspace); + return JSON.parse( + runFileHistoryGit(gitDir, workspace, ["cat-file", "blob", `${checkpointHash}:.deepcode-file-history.json`]) + ); +} + function runFileHistoryGit( gitDir: string, workspace: string, From 1baad75b14f74059fa4398799d9ce6a37e94df6e Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 28 May 2026 17:34:40 +0800 Subject: [PATCH 103/212] =?UTF-8?q?fix(prompt-buffer):=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20getCurrentSlashToken=20=E5=87=BD=E6=95=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化判断逻辑,只有当文本以斜杠开头时才返回完整文本 - 移除按行处理逻辑,直接返回整个文本 - 修改测试用例以匹配新的行为 - 确保多行文本中不以斜杠开头时返回 null --- src/tests/prompt-buffer.test.ts | 8 ++++---- src/ui/core/prompt-buffer.ts | 14 ++------------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/tests/prompt-buffer.test.ts b/src/tests/prompt-buffer.test.ts index 67ac23af..19537f7e 100644 --- a/src/tests/prompt-buffer.test.ts +++ b/src/tests/prompt-buffer.test.ts @@ -106,14 +106,14 @@ test("getCurrentSlashToken returns the slash word at the cursor", () => { assert.equal(getCurrentSlashToken(buffer), "/skill"); }); -test("getCurrentSlashToken returns null when token contains whitespace", () => { +test("getCurrentSlashToken returns full text when it starts with /", () => { const buffer = { text: "/skill foo", cursor: 10 }; - assert.equal(getCurrentSlashToken(buffer), null); + assert.equal(getCurrentSlashToken(buffer), "/skill foo"); }); -test("getCurrentSlashToken supports slash on a new line", () => { +test("getCurrentSlashToken returns null when text starts on a new line with /", () => { const buffer = { text: "do this\n/n", cursor: 10 }; - assert.equal(getCurrentSlashToken(buffer), "/n"); + assert.equal(getCurrentSlashToken(buffer), null); }); test("getCurrentSlashToken returns null when no slash prefix", () => { diff --git a/src/ui/core/prompt-buffer.ts b/src/ui/core/prompt-buffer.ts index 3e0a710b..c3249304 100644 --- a/src/ui/core/prompt-buffer.ts +++ b/src/ui/core/prompt-buffer.ts @@ -155,20 +155,10 @@ export function isEmpty(state: PromptBufferState): boolean { export function getCurrentSlashToken(state: PromptBufferState): string | null { const text = state.text; - if (text.length === 0) { + if (text.length === 0 || !text.startsWith("/")) { return null; } - const beforeCursor = text.slice(0, state.cursor); - const lastNewline = beforeCursor.lastIndexOf("\n"); - const lineStart = lastNewline + 1; - const line = beforeCursor.slice(lineStart); - if (!line.startsWith("/")) { - return null; - } - if (/\s/.test(line)) { - return null; - } - return line; + return text; } /** From 933e9775b81dd909de72232bc635e6a6eefc74ef Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 28 May 2026 17:52:10 +0800 Subject: [PATCH 104/212] feat: add user prompt checkpointing and notify on manual file changes --- src/common/file-history.ts | 36 +++++-- src/session.ts | 10 +- src/tests/session.test.ts | 213 +++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 9 deletions(-) diff --git a/src/common/file-history.ts b/src/common/file-history.ts index b4a73046..c137fe6f 100644 --- a/src/common/file-history.ts +++ b/src/common/file-history.ts @@ -18,6 +18,11 @@ type FileHistoryManifest = { files: Record; }; +export type FileHistoryCheckpointResult = { + checkpointHash: string | undefined; + changedFilePaths: string[]; +}; + export class GitFileHistory { constructor( _projectRoot: string, @@ -114,21 +119,33 @@ export class GitFileHistory { } } - recordTrackedFilesCheckpoint(sessionId: string, message: string): string | undefined { + recordTrackedFilesCheckpoint(sessionId: string, message: string): FileHistoryCheckpointResult { const currentHash = this.ensureSession(sessionId); if (!currentHash) { - return undefined; + return { checkpointHash: undefined, changedFilePaths: [] }; } try { const manifest = this.readManifest(currentHash); - const trackedPaths = Object.values(manifest.files).map((entry) => entry.path); + const trackedPaths = Object.values(manifest.files) + .map((entry) => entry.path) + .sort((left, right) => left.localeCompare(right)); if (trackedPaths.length === 0) { - return currentHash; + return { checkpointHash: currentHash, changedFilePaths: [] }; } - return this.recordCheckpoint(sessionId, trackedPaths, message); + const nextHash = this.recordCheckpoint(sessionId, trackedPaths, message); + if (!nextHash) { + return { checkpointHash: undefined, changedFilePaths: [] }; + } + + const nextManifest = this.readManifest(nextHash); + const changedFilePaths = Object.entries(manifest.files) + .filter(([key, entry]) => !isSameFileHistoryEntry(entry, nextManifest.files[key])) + .map(([key, entry]) => nextManifest.files[key]?.path ?? entry.path) + .sort((left, right) => left.localeCompare(right)); + return { checkpointHash: nextHash, changedFilePaths }; } catch { - return undefined; + return { checkpointHash: undefined, changedFilePaths: [] }; } } @@ -340,6 +357,13 @@ function normalizeManifest(manifest: FileHistoryManifest): FileHistoryManifest { return { version: 2, files }; } +function isSameFileHistoryEntry(left: FileHistoryEntry, right: FileHistoryEntry | undefined): boolean { + if (!right) { + return false; + } + return left.path === right.path && left.blob === right.blob && left.mode === right.mode; +} + function uniqueAbsolutePaths(filePaths: string[]): string[] { return Array.from(new Set(filePaths.map((filePath) => path.resolve(filePath)))); } diff --git a/src/session.ts b/src/session.ts index fed4db5b..8dc6fc65 100644 --- a/src/session.ts +++ b/src/session.ts @@ -30,7 +30,7 @@ import type { McpServerConfig, PermissionScope, PermissionSettings } from "./set import { logApiError } from "./common/error-logger"; import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; import { killProcessTree } from "./common/process-tree"; -import { GitFileHistory } from "./common/file-history"; +import { GitFileHistory, type FileHistoryCheckpointResult } from "./common/file-history"; import { clearSessionState, getSnippet, rebuildSessionStateFromHistory } from "./common/state"; import { appendProjectPermissionAllows, @@ -1088,7 +1088,11 @@ ${skillMd} this.reportNewPrompt(); this.ensureFileHistorySession(sessionId); - this.recordUserPromptCheckpoint(sessionId); + const checkpoint = this.recordUserPromptCheckpoint(sessionId); + if (checkpoint.changedFilePaths.length) { + const content = `Note that the user manually modified these files:\n${checkpoint.changedFilePaths.join("\n")}`; + this.appendSessionMessage(sessionId, this.buildSystemMessage(sessionId, content)); + } const userMessage = this.buildUserMessage(sessionId, userPrompt); this.appendSessionMessage(sessionId, userMessage); @@ -1754,7 +1758,7 @@ ${skillMd} return this.getFileHistory().getCurrentCheckpointHash(sessionId); } - private recordUserPromptCheckpoint(sessionId: string): string | undefined { + private recordUserPromptCheckpoint(sessionId: string): FileHistoryCheckpointResult { return this.getFileHistory().recordTrackedFilesCheckpoint(sessionId, "User prompt checkpoint"); } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 0de25524..cb1b8477 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1061,6 +1061,219 @@ test("replySession snapshots manual edits to tracked files before appending the assert.equal(fs.readFileSync(filePath, "utf8"), manualEdit); }); +test("replySession inserts hidden system notice for manually changed tracked files", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-manual-change-notice-workspace-"); + const home = createTempDir("deepcode-manual-change-notice-home-"); + setHomeDir(home); + + const firstPath = path.join(workspace, "a.txt"); + const secondPath = path.join(workspace, "b.txt"); + const manager = createSessionManager(workspace, "machine-id-manual-change-notice"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const fileHistory = new GitFileHistory(workspace, getFileHistoryGitDir(home, workspace)); + fs.writeFileSync(firstPath, "one\n", "utf8"); + fs.writeFileSync(secondPath, "two\n", "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [secondPath, firstPath], "track files")); + + fs.writeFileSync(secondPath, "two changed\n", "utf8"); + fs.writeFileSync(firstPath, "one changed\n", "utf8"); + await manager.replySession(sessionId, { text: "check manual changes" }); + + const messages = manager.listSessionMessages(sessionId); + const userIndex = messages.findIndex( + (message) => message.role === "user" && message.content === "check manual changes" + ); + assert.ok(userIndex > 0); + const notice = messages[userIndex - 1]; + assert.equal(notice?.role, "system"); + assert.equal(notice?.visible, false); + assert.equal(notice?.content, `Note that the user manually modified these files:\n${firstPath}\n${secondPath}`); +}); + +test("replySession does not insert manual-change notice when tracked files are unchanged", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-no-manual-change-notice-workspace-"); + const home = createTempDir("deepcode-no-manual-change-notice-home-"); + setHomeDir(home); + + const filePath = path.join(workspace, "tracked.txt"); + const manager = createSessionManager(workspace, "machine-id-no-manual-change-notice"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const fileHistory = new GitFileHistory(workspace, getFileHistoryGitDir(home, workspace)); + fs.writeFileSync(filePath, "same\n", "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [filePath], "track file")); + + await manager.replySession(sessionId, { text: "second prompt" }); + + const notices = manager + .listSessionMessages(sessionId) + .filter( + (message) => + message.role === "system" && + typeof message.content === "string" && + message.content.startsWith("Note that the user manually modified these files:") + ); + assert.equal(notices.length, 0); +}); + +test("replySession reports manual deletion of a tracked file", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-manual-delete-notice-workspace-"); + const home = createTempDir("deepcode-manual-delete-notice-home-"); + setHomeDir(home); + + const filePath = path.join(workspace, "deleted.txt"); + const manager = createSessionManager(workspace, "machine-id-manual-delete-notice"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const fileHistory = new GitFileHistory(workspace, getFileHistoryGitDir(home, workspace)); + fs.writeFileSync(filePath, "delete me\n", "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [filePath], "track file")); + + fs.unlinkSync(filePath); + await manager.replySession(sessionId, { text: "check deletion" }); + + const notice = manager + .listSessionMessages(sessionId) + .find( + (message) => + message.role === "system" && + message.content === `Note that the user manually modified these files:\n${filePath}` + ); + assert.ok(notice); +}); + +test("replySession ignores manually created untracked files", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-untracked-manual-file-workspace-"); + const home = createTempDir("deepcode-untracked-manual-file-home-"); + setHomeDir(home); + + const trackedPath = path.join(workspace, "tracked.txt"); + const untrackedPath = path.join(workspace, "untracked.txt"); + const manager = createSessionManager(workspace, "machine-id-untracked-manual-file"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const fileHistory = new GitFileHistory(workspace, getFileHistoryGitDir(home, workspace)); + fs.writeFileSync(trackedPath, "tracked\n", "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [trackedPath], "track file")); + + fs.writeFileSync(untrackedPath, "new manual file\n", "utf8"); + await manager.replySession(sessionId, { text: "second prompt" }); + + const notices = manager + .listSessionMessages(sessionId) + .filter( + (message) => + message.role === "system" && + typeof message.content === "string" && + message.content.startsWith("Note that the user manually modified these files:") + ); + assert.equal(notices.length, 0); +}); + +test("replySession does not insert manual-change notice for /continue", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-continue-no-manual-change-notice-workspace-"); + const home = createTempDir("deepcode-continue-no-manual-change-notice-home-"); + setHomeDir(home); + + const filePath = path.join(workspace, "tracked.txt"); + const manager = createSessionManager(workspace, "machine-id-continue-no-manual-change-notice"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const fileHistory = new GitFileHistory(workspace, getFileHistoryGitDir(home, workspace)); + fs.writeFileSync(filePath, "before\n", "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [filePath], "track file")); + + fs.writeFileSync(filePath, "manual change\n", "utf8"); + await manager.replySession(sessionId, { text: "/continue" }); + + const notices = manager + .listSessionMessages(sessionId) + .filter( + (message) => + message.role === "system" && + typeof message.content === "string" && + message.content.startsWith("Note that the user manually modified these files:") + ); + assert.equal(notices.length, 0); +}); + +test("replySession does not insert manual-change notice for permission-only replies", async (t) => { + if (!hasGit()) { + t.skip("git is not available"); + return; + } + + const workspace = createTempDir("deepcode-permission-no-manual-change-notice-workspace-"); + const home = createTempDir("deepcode-permission-no-manual-change-notice-home-"); + setHomeDir(home); + + const filePath = path.join(workspace, "tracked.txt"); + const manager = createSessionManager(workspace, "machine-id-permission-no-manual-change-notice"); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "first prompt" }); + const fileHistory = new GitFileHistory(workspace, getFileHistoryGitDir(home, workspace)); + fs.writeFileSync(filePath, "before\n", "utf8"); + assert.ok(fileHistory.recordCheckpoint(sessionId, [filePath], "track file")); + const assistant = (manager as any).buildAssistantMessage( + sessionId, + "Need permission", + [ + { + id: "call-read", + type: "function", + function: { name: "read", arguments: JSON.stringify({ file_path: filePath }) }, + }, + ], + null + ) as SessionMessage; + (manager as any).appendSessionMessage(sessionId, assistant); + + fs.writeFileSync(filePath, "manual change\n", "utf8"); + await manager.replySession(sessionId, { permissions: [{ toolCallId: "call-read", permission: "allow" }] }); + + const notices = manager + .listSessionMessages(sessionId) + .filter( + (message) => + message.role === "system" && + typeof message.content === "string" && + message.content.startsWith("Note that the user manually modified these files:") + ); + assert.equal(notices.length, 0); +}); + test("Write tool advances file-history while preserving the user prompt checkpoint", async (t) => { if (!hasGit()) { t.skip("git is not available"); From 39b48e1041cdc6866deff22abfa979ceba9cf662 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 28 May 2026 18:03:20 +0800 Subject: [PATCH 105/212] feat: enhance edit tool to handle empty old_string cases for file edits --- src/tests/tool-handlers.test.ts | 46 +++++++++++++++++++++++++++++++++ src/tools/edit-handler.ts | 41 ++++++++++++++++++++++------- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/tests/tool-handlers.test.ts b/src/tests/tool-handlers.test.ts index da1cb9c9..d73d10ff 100644 --- a/src/tests/tool-handlers.test.ts +++ b/src/tests/tool-handlers.test.ts @@ -607,6 +607,52 @@ test("Edit requires snippet_id even after Write refreshes file state", async () assert.equal(fs.readFileSync(filePath, "utf8"), "alpha\nbeta\n"); }); +test("Edit allows empty old_string when the file is empty", async () => { + const workspace = createTempWorkspace(); + const filePath = path.join(workspace, "empty-edit.txt"); + fs.writeFileSync(filePath, "", "utf8"); + + const sessionId = "edit-empty-existing"; + const snippet = await readSnippet(filePath, sessionId, workspace); + + const editResult = await handleEditTool( + { + snippet_id: snippet.id, + old_string: "", + new_string: "initialized\n", + }, + createContext(sessionId, workspace) + ); + + assert.equal(editResult.ok, true); + assert.equal(editResult.metadata?.matched_via, "empty_file"); + assert.equal(editResult.metadata?.replaced_count, 1); + assert.match(String(editResult.metadata?.diff_preview ?? ""), /\+initialized/); + assert.equal(fs.readFileSync(filePath, "utf8"), "initialized\n"); +}); + +test("Edit rejects empty old_string when the file is not empty", async () => { + const workspace = createTempWorkspace(); + const filePath = path.join(workspace, "non-empty-edit.txt"); + fs.writeFileSync(filePath, "alpha\n", "utf8"); + + const sessionId = "edit-empty-old-string-non-empty-file"; + const snippet = await readSnippet(filePath, sessionId, workspace); + + const editResult = await handleEditTool( + { + snippet_id: snippet.id, + old_string: "", + new_string: "initialized\n", + }, + createContext(sessionId, workspace) + ); + + assert.equal(editResult.ok, false); + assert.equal(editResult.error, "old_string must not be empty unless the file is empty."); + assert.equal(fs.readFileSync(filePath, "utf8"), "alpha\n"); +}); + test("Write requires a full read before overwriting an existing file", async () => { const workspace = createTempWorkspace(); const filePath = path.join(workspace, "config.txt"); diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index dec0c160..eb133dae 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -126,14 +126,6 @@ export async function handleEditTool( }; } - if (input.old_string === "") { - return { - ok: false, - name: "edit", - error: "old_string must not be empty.", - }; - } - if (input.old_string === input.new_string) { return { ok: false, @@ -195,11 +187,40 @@ export async function handleEditTool( const replaceAll = input.replace_all ?? false; const lineIndex = buildLineIndex(raw); const scope = buildSearchScope(filePath, raw, lineIndex, snippet); - let matches = findOccurrences(raw, oldString, scope); - let matchedVia: "exact" | "line_leading_tab_correction" | "loose_escape" | "llm_escape_correction" = "exact"; + let matches: MatchOccurrence[] = []; + let matchedVia: + | "exact" + | "empty_file" + | "line_leading_tab_correction" + | "loose_escape" + | "llm_escape_correction" = "exact"; let replacementOldString = oldString; let replacementNewString = newString; + if (oldString === "") { + if (raw !== "") { + return { + ok: false, + name: "edit", + error: "old_string must not be empty unless the file is empty.", + metadata: { + scope: formatScopeMetadata(scope), + }, + }; + } + matches = [ + { + startOffset: 0, + endOffset: 0, + startLine: 1, + endLine: 1, + }, + ]; + matchedVia = "empty_file"; + } else { + matches = findOccurrences(raw, oldString, scope); + } + if (matches.length === 0) { const tabStrippedOldString = stripReadResultLineTabs(oldString); if (tabStrippedOldString !== oldString) { From d3c991c1c9d7e41930eed7e97c905796f05282eb Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 28 May 2026 19:58:57 +0800 Subject: [PATCH 106/212] feat: implement project code management for Windows compatibility and enhance debug logger tests --- src/session.ts | 38 ++++++++++++++++++++++++++---- src/tests/debug-logger.test.ts | 9 +++++++ src/tests/session.test.ts | 43 +++++++++++++++++----------------- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/src/session.ts b/src/session.ts index 8dc6fc65..6c73e37b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -57,6 +57,8 @@ export type { } from "./common/permissions"; const MAX_SESSION_ENTRIES = 50; +const MAX_PROJECT_CODE_LENGTH = 64; +const PROJECT_CODE_HASH_LENGTH = 16; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; @@ -75,6 +77,36 @@ export function getCompactPromptTokenThreshold(model: string): number { : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; } +// Keep project storage paths short enough for Git's internal files on Windows. +export function getProjectCode(projectRoot: string): string { + const legacyCode = getLegacyProjectCode(projectRoot); + if (legacyCode.length <= MAX_PROJECT_CODE_LENGTH) { + return legacyCode; + } + + const normalizedRoot = path.resolve(projectRoot); + const hashInput = process.platform === "win32" ? normalizedRoot.toLowerCase() : normalizedRoot; + const hash = crypto.createHash("sha256").update(hashInput).digest("hex").slice(0, PROJECT_CODE_HASH_LENGTH); + const prefixLimit = MAX_PROJECT_CODE_LENGTH - PROJECT_CODE_HASH_LENGTH - 1; + const basename = path.basename(normalizedRoot); + const prefix = + sanitizeProjectCodePart(basename) + .slice(0, prefixLimit) + .replace(/[-.]+$/g, "") || "project"; + return `${prefix}-${hash}`; +} + +function getLegacyProjectCode(projectRoot: string): string { + return projectRoot.replace(/[\\/]/g, "-").replace(/:/g, ""); +} + +function sanitizeProjectCodePart(value: string): string { + return value + .replace(/[^A-Za-z0-9._-]/g, "-") + .replace(/-+/g, "-") + .replace(/^[-.]+|[-.]+$/g, ""); +} + function isUsageRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -1726,16 +1758,12 @@ ${skillMd} }; } - private getProjectCode(projectRoot: string): string { - return projectRoot.replace(/[\\/]/g, "-").replace(/:/g, ""); - } - private getProjectStorage(): { projectCode: string; projectDir: string; sessionsIndexPath: string; } { - const projectCode = this.getProjectCode(this.projectRoot); + const projectCode = getProjectCode(this.projectRoot); const projectDir = path.join(os.homedir(), ".deepcode", "projects", projectCode); const sessionsIndexPath = path.join(projectDir, "sessions-index.json"); return { projectCode, projectDir, sessionsIndexPath }; diff --git a/src/tests/debug-logger.test.ts b/src/tests/debug-logger.test.ts index 7b1aad40..3d42ff97 100644 --- a/src/tests/debug-logger.test.ts +++ b/src/tests/debug-logger.test.ts @@ -7,8 +7,12 @@ import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/debug-l test("debug logger appends full entries without rotation", () => { const originalHome = process.env.HOME; + const originalUserProfile = process.env.USERPROFILE; const home = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-debug-log-home-")); process.env.HOME = home; + if (process.platform === "win32") { + process.env.USERPROFILE = home; + } try { for (let index = 0; index < 25; index += 1) { logOpenAIChatCompletionDebug({ @@ -42,5 +46,10 @@ test("debug logger appends full entries without rotation", () => { } else { process.env.HOME = originalHome; } + if (originalUserProfile === undefined) { + delete process.env.USERPROFILE; + } else { + process.env.USERPROFILE = originalUserProfile; + } } }); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index cb1b8477..b3cbe529 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -6,8 +6,7 @@ import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/file-history"; import { clearSessionState } from "../common/state"; -import { type SessionMessage } from "../session"; -import { SessionManager } from "../session"; +import { getProjectCode, SessionManager, type SessionMessage } from "../session"; const originalFetch = globalThis.fetch; const originalConsoleWarn = console.warn; @@ -45,6 +44,21 @@ afterEach(() => { } }); +test("getProjectCode shortens long project roots for Windows-compatible storage paths", () => { + const shortRoot = "short-project"; + assert.equal(getProjectCode(shortRoot), shortRoot.replace(/[\\/]/g, "-").replace(/:/g, "")); + + const longRoot = path.join( + os.tmpdir(), + "deepcode-project-code-workspace-with-a-long-name-that-would-create-long-git-internal-paths" + ); + const projectCode = getProjectCode(longRoot); + + assert.ok(projectCode.length <= 64); + assert.match(projectCode, /^[A-Za-z0-9._-]+$/); + assert.notEqual(projectCode, longRoot.replace(/[\\/]/g, "-").replace(/:/g, "")); +}); + test("SessionManager preserves structured system content when building OpenAI messages", () => { const manager = new SessionManager({ projectRoot: process.cwd(), @@ -271,7 +285,7 @@ test("SessionManager normalizes legacy sessions without activeTokens to zero", ( const home = createTempDir("deepcode-legacy-active-tokens-home-"); setHomeDir(home); - const projectCode = workspace.replace(/[\\/]/g, "-").replace(/:/g, ""); + const projectCode = getProjectCode(workspace); const projectDir = path.join(home, ".deepcode", "projects", projectCode); fs.mkdirSync(projectDir, { recursive: true }); fs.writeFileSync( @@ -324,7 +338,7 @@ test("SessionManager marks skills loaded from existing session messages", async "utf8" ); - const projectCode = workspace.replace(/[\\/]/g, "-").replace(/:/g, ""); + const projectCode = getProjectCode(workspace); const projectDir = path.join(home, ".deepcode", "projects", projectCode); fs.mkdirSync(projectDir, { recursive: true }); fs.writeFileSync( @@ -982,14 +996,7 @@ test("createSession initializes file-history repo and session branch", async (t) const sessionId = await manager.createSession({ text: "first prompt" }); const userMessage = manager.listSessionMessages(sessionId).find((message) => message.role === "user"); - const gitDir = path.join( - home, - ".deepcode", - "projects", - workspace.replace(/[\\/]/g, "-").replace(/:/g, ""), - "file-history", - ".git" - ); + const gitDir = path.join(home, ".deepcode", "projects", getProjectCode(workspace), "file-history", ".git"); assert.ok(fs.existsSync(gitDir)); assert.ok(userMessage?.checkpointHash); @@ -2850,13 +2857,7 @@ test("SessionManager.deleteSession removes the messages file", () => { (manager as any).activateSession = async () => {}; const sessionId = createSessionAndMessages(manager, "session-delete-msg", "Test session"); - const messagePath = path.join( - home, - ".deepcode", - "projects", - workspace.replace(/[\\\\/]/g, "-").replace(/:/g, ""), - `${sessionId}.jsonl` - ); + const messagePath = path.join(home, ".deepcode", "projects", getProjectCode(workspace), `${sessionId}.jsonl`); // Verify messages file exists assert.ok(fs.existsSync(messagePath)); @@ -2962,7 +2963,7 @@ function createFileHistoryCommit( sessionId: string, files: Record ): string { - const projectCode = workspace.replace(/[\\/]/g, "-").replace(/:/g, ""); + const projectCode = getProjectCode(workspace); const gitDir = path.join(home, ".deepcode", "projects", projectCode, "file-history", ".git"); const fileHistory = new GitFileHistory(workspace, gitDir); fileHistory.ensureSession(sessionId); @@ -2980,7 +2981,7 @@ function createFileHistoryCommit( } function getFileHistoryGitDir(home: string, workspace: string): string { - const projectCode = workspace.replace(/[\\/]/g, "-").replace(/:/g, ""); + const projectCode = getProjectCode(workspace); return path.join(home, ".deepcode", "projects", projectCode, "file-history", ".git"); } From 4cb25b32e64ffa5b74d623efa7c6ebbbcecc4191 Mon Sep 17 00:00:00 2001 From: lellansin Date: Thu, 28 May 2026 22:59:44 +0800 Subject: [PATCH 107/212] refactor: extract telemetry into separate module with enable/disable toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create src/common/telemetry.ts — standalone reportNewPrompt() with configurable enabled flag - Add telemetryEnabled to settings types and resolution chain (DeepcodingSettings, ResolvedDeepcodingSettings, CreateOpenAIClient) - Resolution priority: system env > project env > project settings > user env > user settings > default true (opt-out) - Configurable via settings.json ("telemetryEnabled": false) or env (DEEPCODE_TELEMETRY_ENABLED=0) - Reduce session.ts by ~20 lines, delegating to shared module --- src/common/openai-client.ts | 4 ++++ src/common/telemetry.ts | 34 ++++++++++++++++++++++++++++++++++ src/session.ts | 24 +++--------------------- src/settings.ts | 12 ++++++++++++ src/tools/executor.ts | 1 + 5 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 src/common/telemetry.ts diff --git a/src/common/openai-client.ts b/src/common/openai-client.ts index ee3dd667..35877835 100644 --- a/src/common/openai-client.ts +++ b/src/common/openai-client.ts @@ -26,6 +26,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { thinkingEnabled: boolean; reasoningEffort: "high" | "max"; debugLogEnabled: boolean; + telemetryEnabled: boolean; notify?: string; webSearchTool?: string; env: Record; @@ -40,6 +41,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { thinkingEnabled: settings.thinkingEnabled, reasoningEffort: settings.reasoningEffort, debugLogEnabled: settings.debugLogEnabled, + telemetryEnabled: settings.telemetryEnabled, notify: settings.notify, webSearchTool: settings.webSearchTool, env: settings.env, @@ -56,6 +58,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { thinkingEnabled: settings.thinkingEnabled, reasoningEffort: settings.reasoningEffort, debugLogEnabled: settings.debugLogEnabled, + telemetryEnabled: settings.telemetryEnabled, notify: settings.notify, webSearchTool: settings.webSearchTool, env: settings.env, @@ -91,6 +94,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { thinkingEnabled: settings.thinkingEnabled, reasoningEffort: settings.reasoningEffort, debugLogEnabled: settings.debugLogEnabled, + telemetryEnabled: settings.telemetryEnabled, notify: settings.notify, webSearchTool: settings.webSearchTool, env: settings.env, diff --git a/src/common/telemetry.ts b/src/common/telemetry.ts new file mode 100644 index 00000000..f6dc60b3 --- /dev/null +++ b/src/common/telemetry.ts @@ -0,0 +1,34 @@ +const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; +const DEFAULT_REPORT_TIMEOUT_MS = 3000; + +export type NewPromptReportOptions = { + enabled: boolean; + machineId?: string; + timeoutMs?: number; +}; + +/** + * Fire-and-forget report of a new prompt session. + * Respects the `enabled` toggle: when disabled, the call is a no-op. + */ +export function reportNewPrompt(options: NewPromptReportOptions): void { + if (!options.enabled || !options.machineId) { + return; + } + + const timeoutMs = options.timeoutMs ?? DEFAULT_REPORT_TIMEOUT_MS; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + void fetch(DEFAULT_NEW_PROMPT_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Token: options.machineId, + }, + body: JSON.stringify({}), + signal: controller.signal, + }) + .catch(() => {}) + .finally(() => clearTimeout(timeout)); +} diff --git a/src/session.ts b/src/session.ts index 6c73e37b..358789e4 100644 --- a/src/session.ts +++ b/src/session.ts @@ -45,6 +45,7 @@ import { type UserToolPermission, } from "./common/permissions"; import { clearSessionWorkingDir } from "./tools/bash-handler"; +import { reportNewPrompt } from "./common/telemetry"; export type { PermissionScope } from "./settings"; export type { @@ -59,8 +60,6 @@ export type { const MAX_SESSION_ENTRIES = 50; const MAX_PROJECT_CODE_LENGTH = 64; const PROJECT_CODE_HASH_LENGTH = 16; -const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; -const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; @@ -1504,25 +1503,8 @@ ${skillMd} } private reportNewPrompt(): void { - const { machineId } = this.createOpenAIClient(); - if (!machineId) { - return; - } - - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), NEW_PROMPT_REPORT_TIMEOUT_MS); - - void fetch(DEFAULT_NEW_PROMPT_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Token: machineId, - }, - body: JSON.stringify({}), - signal: controller.signal, - }) - .catch(() => {}) - .finally(() => clearTimeout(timeout)); + const { machineId, telemetryEnabled } = this.createOpenAIClient(); + reportNewPrompt({ enabled: telemetryEnabled ?? true, machineId }); } interruptActiveSession(): void { diff --git a/src/settings.ts b/src/settings.ts index b7a7a777..14755dd1 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -10,6 +10,7 @@ export type DeepcodingEnv = Record & { THINKING_ENABLED?: string; REASONING_EFFORT?: string; DEBUG_LOG_ENABLED?: string; + TELEMETRY_ENABLED?: string; }; export type ReasoningEffort = "high" | "max"; @@ -47,6 +48,7 @@ export type DeepcodingSettings = { thinkingEnabled?: boolean; reasoningEffort?: ReasoningEffort; debugLogEnabled?: boolean; + telemetryEnabled?: boolean; notify?: string; webSearchTool?: string; mcpServers?: Record; @@ -61,6 +63,7 @@ export type ResolvedDeepcodingSettings = { thinkingEnabled: boolean; reasoningEffort: ReasoningEffort; debugLogEnabled: boolean; + telemetryEnabled: boolean; notify?: string; webSearchTool?: string; mcpServers?: Record; @@ -313,6 +316,14 @@ export function resolveSettingsSources( parseBoolean(userEnv.DEBUG_LOG_ENABLED) ?? false; + const telemetryEnabled = + parseBoolean(systemEnv.TELEMETRY_ENABLED) ?? + parseBoolean(projectSettings?.telemetryEnabled) ?? + parseBoolean(projectEnv.TELEMETRY_ENABLED) ?? + parseBoolean(userSettings?.telemetryEnabled) ?? + parseBoolean(userEnv.TELEMETRY_ENABLED) ?? + true; + const notify = trimString(systemEnv.NOTIFY) || trimString(projectSettings?.notify) || trimString(userSettings?.notify) || ""; const webSearchTool = @@ -329,6 +340,7 @@ export function resolveSettingsSources( thinkingEnabled, reasoningEffort, debugLogEnabled, + telemetryEnabled, notify: notify || undefined, webSearchTool: webSearchTool || undefined, mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv), diff --git a/src/tools/executor.ts b/src/tools/executor.ts index 220fc894..155c8724 100644 --- a/src/tools/executor.ts +++ b/src/tools/executor.ts @@ -16,6 +16,7 @@ export type CreateOpenAIClient = () => { thinkingEnabled: boolean; reasoningEffort?: ReasoningEffort; debugLogEnabled?: boolean; + telemetryEnabled?: boolean; notify?: string; webSearchTool?: string; env?: Record; From b906283212bd732a1d4b3c31e4d5d1ffbb60f8bb Mon Sep 17 00:00:00 2001 From: lellansin Date: Thu, 28 May 2026 23:03:21 +0800 Subject: [PATCH 108/212] docs: add telemetryEnabled configuration option to both zh/en docs --- docs/configuration.md | 12 ++++++++++++ docs/configuration_en.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 1cce9a14..922f39e3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -31,6 +31,7 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 | `thinkingEnabled` | boolean | 是否启用思考模式(DeepSeek V4 系列默认启用) | | `reasoningEffort` | string | 推理强度,可选 `"high"` 或 `"max"`(默认 `"max"`) | | `debugLogEnabled` | boolean | 是否启用调试日志输出(默认 `false`) | +| `telemetryEnabled` | boolean | 是否启用匿名使用数据上报(默认 `true`) | | `notify` | string | 任务完成通知脚本的完整路径(如 Slack 通知脚本) | | `webSearchTool` | string | 自定义联网搜索脚本的完整路径 | | `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) | @@ -45,6 +46,7 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 | `THINKING_ENABLED` | string | 是否启用思考模式 | | `REASONING_EFFORT` | string | 推理强度 | | `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 | +| `TELEMETRY_ENABLED` | string | 是否启用匿名使用数据上报 | | `<其他任意KEY>` | string | 自定义环境变量 | #### `thinkingEnabled` — 思考模式 @@ -130,6 +132,16 @@ MCP(Model Context Protocol)服务器配置。值是键值对,键为服务 设为 `true` 可让程序输出详细的调试日志(默认 `false`),用于排查 API 调用和工具执行的问题。 +#### `telemetryEnabled` — 匿名使用数据上报 + +设为 `false` 可关闭匿名使用数据上报(默认 `true`)。上报仅包含匿名的机器标识,不包含对话内容、代码或 API 密钥。 + +也可以通过环境变量关闭: + +```bash +DEEPCODE_TELEMETRY_ENABLED=0 deepcode +``` + ## 环境变量优先级 环境变量是配置应用程序的常用方式,尤其适用于敏感信息(如 api-key)或可能在不同环境之间更改的设置。 diff --git a/docs/configuration_en.md b/docs/configuration_en.md index fa396f90..f53fb114 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -31,6 +31,7 @@ The following are all the top-level fields supported in `settings.json`, along w | `thinkingEnabled` | boolean | Whether to enable thinking mode (enabled by default for DeepSeek V4 series)| | `reasoningEffort` | string | Reasoning intensity, either `"high"` or `"max"` (default `"max"`) | | `debugLogEnabled` | boolean | Enable debug log output (default `false`) | +| `telemetryEnabled` | boolean | Enable anonymous usage reporting (default `true`) | | `notify` | string | Full path to a task-completion notification script (e.g., Slack notification script) | | `webSearchTool` | string | Full path to a custom web search script | | `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) | @@ -45,6 +46,7 @@ The following are all the top-level fields supported in `settings.json`, along w | `THINKING_ENABLED`| string | Enable thinking mode | | `REASONING_EFFORT`| string | Reasoning intensity | | `DEBUG_LOG_ENABLED`| string| Enable debug log output | +| `TELEMETRY_ENABLED`| string| Enable anonymous usage reporting | | `` | string | Custom environment variable | #### `thinkingEnabled` — Thinking Mode @@ -129,6 +131,16 @@ For detailed MCP usage instructions, refer to [mcp.md](mcp.md). Set to `true` to enable detailed debug logging (default `false`), useful for troubleshooting API calls and tool execution. +#### `telemetryEnabled` — Anonymous Usage Reporting + +Set to `false` to disable anonymous usage reporting (default `true`). The report only includes an anonymous machine identifier and does not contain conversation content, code, or API keys. + +You can also disable it via environment variable: + +```bash +DEEPCODE_TELEMETRY_ENABLED=0 deepcode +``` + ## Environment Variable Priority Environment variables are a common way to configure applications, especially for sensitive information (such as api-key) or settings that may change between environments. From c612c927cb1d27150b1945378e7a6f4952ebc005 Mon Sep 17 00:00:00 2001 From: lellansin Date: Thu, 28 May 2026 23:09:16 +0800 Subject: [PATCH 109/212] test: add telemetry module and settings resolution unit tests - src/tests/telemetry.test.ts: 6 tests covering enabled flag, missing machineId, correct fetch params, error swallowing, custom timeout - src/tests/settings-and-notify.test.ts: 3 new tests + extended precedence test verifying telemetryEnabled resolution chain --- src/tests/settings-and-notify.test.ts | 34 ++++++++ src/tests/telemetry.test.ts | 109 ++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/tests/telemetry.test.ts diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts index 52f8671d..9e18dc1c 100644 --- a/src/tests/settings-and-notify.test.ts +++ b/src/tests/settings-and-notify.test.ts @@ -83,6 +83,36 @@ test("resolveSettings reads THINKING_ENABLED, REASONING_EFFORT, and DEBUG_LOG_EN assert.equal(resolved.baseURL, "https://default.example.com"); }); +test("resolveSettings defaults telemetryEnabled to true", () => { + const resolved = resolveSettings( + {}, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.equal(resolved.telemetryEnabled, true); +}); + +test("resolveSettings reads TELEMETRY_ENABLED from env", () => { + const resolved = resolveSettings( + { env: { TELEMETRY_ENABLED: "0" } }, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.equal(resolved.telemetryEnabled, false); +}); + +test("resolveSettings gives top-level telemetryEnabled priority over env TELEMETRY_ENABLED", () => { + const resolved = resolveSettings( + { + telemetryEnabled: false, + env: { TELEMETRY_ENABLED: "true" }, + }, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.equal(resolved.telemetryEnabled, false); +}); + test("resolveSettings ignores removed legacy env.THINKING", () => { const resolved = resolveSettings( { @@ -115,6 +145,7 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre thinkingEnabled: true, reasoningEffort: "max", debugLogEnabled: true, + telemetryEnabled: false, }, { env: { @@ -125,6 +156,7 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre }, model: "project-top-model", thinkingEnabled: true, + telemetryEnabled: true, }, { model: "default-model", @@ -135,6 +167,7 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre DEEPCODE_THINKING_ENABLED: "false", DEEPCODE_REASONING_EFFORT: "high", DEEPCODE_DEBUG_LOG_ENABLED: "true", + DEEPCODE_TELEMETRY_ENABLED: "false", DEEPCODE_WEBHOOK: "system-webhook", } ); @@ -144,6 +177,7 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre assert.equal(resolved.thinkingEnabled, false); assert.equal(resolved.reasoningEffort, "high"); assert.equal(resolved.debugLogEnabled, true); + assert.equal(resolved.telemetryEnabled, false); assert.equal(resolved.env.WEBHOOK, "system-webhook"); }); diff --git a/src/tests/telemetry.test.ts b/src/tests/telemetry.test.ts new file mode 100644 index 00000000..6db0261e --- /dev/null +++ b/src/tests/telemetry.test.ts @@ -0,0 +1,109 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { reportNewPrompt } from "../common/telemetry"; + +test("reportNewPrompt does not call fetch when enabled is false", () => { + let called = false; + const originalFetch = globalThis.fetch; + globalThis.fetch = ((..._args: unknown[]) => { + called = true; + return Promise.resolve(new Response()); + }) as typeof globalThis.fetch; + + try { + reportNewPrompt({ enabled: false, machineId: "test-machine" }); + assert.equal(called, false); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("reportNewPrompt does not call fetch when machineId is undefined", () => { + let called = false; + const originalFetch = globalThis.fetch; + globalThis.fetch = ((..._args: unknown[]) => { + called = true; + return Promise.resolve(new Response()); + }) as typeof globalThis.fetch; + + try { + reportNewPrompt({ enabled: true }); + assert.equal(called, false); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("reportNewPrompt does not call fetch when machineId is empty string", () => { + let called = false; + const originalFetch = globalThis.fetch; + globalThis.fetch = ((..._args: unknown[]) => { + called = true; + return Promise.resolve(new Response()); + }) as typeof globalThis.fetch; + + try { + reportNewPrompt({ enabled: true, machineId: "" }); + assert.equal(called, false); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("reportNewPrompt calls fetch with correct URL, method, headers, and body", async () => { + const calls: Array<{ url: string; init: RequestInit }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = ((url: string | URL | Request, init?: RequestInit) => { + calls.push({ url: String(url), init: init ?? {} }); + return Promise.resolve(new Response()); + }) as typeof globalThis.fetch; + + try { + reportNewPrompt({ enabled: true, machineId: "test-machine" }); + + // Wait for the fire-and-forget fetch to settle. + await new Promise((resolve) => setTimeout(resolve, 50)); + + assert.equal(calls.length, 1); + assert.equal(calls[0].url, "https://deepcode.vegamo.cn/api/plugin/new"); + assert.equal(calls[0].init.method, "POST"); + assert.equal((calls[0].init.headers as Record)["Content-Type"], "application/json"); + assert.equal((calls[0].init.headers as Record)["Token"], "test-machine"); + assert.equal(calls[0].init.body, JSON.stringify({})); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("reportNewPrompt swallows fetch errors without throwing", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (() => { + return Promise.reject(new Error("Network error")); + }) as typeof globalThis.fetch; + + try { + // Should not throw. + reportNewPrompt({ enabled: true, machineId: "test-machine" }); + await new Promise((resolve) => setTimeout(resolve, 50)); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("reportNewPrompt respects custom timeoutMs", async () => { + const calls: Array<{ signal: AbortSignal }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = ((_url: string | URL | Request, init?: RequestInit) => { + calls.push({ signal: init?.signal as AbortSignal }); + return Promise.resolve(new Response()); + }) as typeof globalThis.fetch; + + try { + reportNewPrompt({ enabled: true, machineId: "test-machine", timeoutMs: 100 }); + await new Promise((resolve) => setTimeout(resolve, 50)); + assert.equal(calls.length, 1); + assert.equal(calls[0].signal.aborted, false); + } finally { + globalThis.fetch = originalFetch; + } +}); From b820bd7370dfee73bf119e06c6e3f20454c4cc05 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 29 May 2026 10:55:09 +0800 Subject: [PATCH 110/212] 0.1.26 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab845a3e..c4044a2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.25", + "version": "0.1.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.25", + "version": "0.1.26", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index 1e93c667..565ba554 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.25", + "version": "0.1.26", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From 402369558585bce937d6944e8d85e88fc8a623ed Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 29 May 2026 15:19:27 +0800 Subject: [PATCH 111/212] feat: replaced findClosestMatch in edit tool with an LLM diagnosis helper --- src/tests/tool-handlers.test.ts | 131 ++++++++++++++++--- src/tools/edit-handler.ts | 219 ++++++++++++++------------------ 2 files changed, 212 insertions(+), 138 deletions(-) diff --git a/src/tests/tool-handlers.test.ts b/src/tests/tool-handlers.test.ts index d73d10ff..aefef2ff 100644 --- a/src/tests/tool-handlers.test.ts +++ b/src/tests/tool-handlers.test.ts @@ -210,7 +210,7 @@ test("Edit returns candidate match snippets when old_string is not unique", asyn assert.match(candidates[0]?.preview ?? "", /city/); }); -test("Edit returns closest matches only above threshold with surrounding context", async () => { +test("Edit reports missing old_string without closest-match metadata when no LLM is configured", async () => { const workspace = createTempWorkspace(); const filePath = path.join(workspace, "closest.ts"); fs.writeFileSync( @@ -239,15 +239,7 @@ test("Edit returns closest matches only above threshold with surrounding context assert.equal(closeResult.ok, false); assert.equal(closeResult.error, "old_string not found in file."); - const closestMatch = closeResult.metadata?.closest_match as - | { snippet_id?: string; start_line?: number; end_line?: number; similarity?: number; preview?: string } - | undefined; - assert.ok(closestMatch?.snippet_id); - assert.equal(closestMatch.start_line, 1); - assert.equal(closestMatch.end_line, 4); - assert.ok((closestMatch.similarity ?? 0) >= 0.8); - assert.match(closestMatch.preview ?? "", /const before = true/); - assert.match(closestMatch.preview ?? "", /return value/); + assert.equal(closeResult.metadata?.closest_match, undefined); const lowResult = await handleEditTool( { @@ -279,12 +271,119 @@ test("Edit returns closest matches only above threshold with surrounding context ); assert.equal(scopedCloseResult.ok, false); - const scopedClosestMatch = scopedCloseResult.metadata?.closest_match as - | { start_line?: number; end_line?: number; preview?: string } - | undefined; - assert.equal(scopedClosestMatch?.start_line, 2); - assert.equal(scopedClosestMatch?.end_line, 3); - assert.doesNotMatch(scopedClosestMatch?.preview ?? "", /const before = true/); + assert.equal(scopedCloseResult.error, "old_string not found in file."); + assert.equal(scopedCloseResult.metadata?.closest_match, undefined); +}); + +test("Edit appends an LLM diagnosis when old_string is not found", async () => { + const workspace = createTempWorkspace(); + const filePath = path.join(workspace, "diagnose.ts"); + fs.writeFileSync( + filePath, + [ + "const beforeOne = true;", + "const beforeTwo = true;", + "function computeSubtotal(value: number) {", + " return value;", + "}", + "const afterOne = true;", + "const afterTwo = true;", + "const afterThree = true;", + ].join("\n"), + "utf8" + ); + + const sessionId = "llm-not-found-diagnosis"; + const readResult = await handleReadTool( + { file_path: filePath, offset: 3, limit: 2 }, + createContext(sessionId, workspace) + ); + const snippet = (readResult.metadata?.snippet ?? null) as { id: string } | null; + assert.ok(snippet); + + let llmCalls = 0; + let prompt = ""; + const editResult = await handleEditTool( + { + snippet_id: snippet.id, + old_string: "function computeTotal(value: number) {\n return value;", + new_string: "function computeTotal(input: number) {\n return input;", + }, + createContext(sessionId, workspace, { + createOpenAIClient: () => ({ + client: { + chat: { + completions: { + create: async (request: { messages?: Array<{ content?: string }> }) => { + llmCalls += 1; + prompt = String(request.messages?.[1]?.content ?? ""); + return { + choices: [ + { + message: { + content: + "", + }, + }, + ], + }; + }, + }, + }, + } as any, + model: "test-model", + thinkingEnabled: false, + }), + }) + ); + + assert.equal(editResult.ok, false); + assert.equal(llmCalls, 1); + assert.equal( + editResult.error, + "old_string not found in file. The requested function name is computeTotal, but the snippet contains computeSubtotal." + ); + assert.equal(editResult.metadata?.closest_match, undefined); + assert.match(prompt, //); + assert.match(prompt, //); + assert.doesNotMatch(prompt, /const afterTwo = true/); +}); + +test("Edit keeps the base not-found error when the LLM diagnosis is unavailable", async () => { + const workspace = createTempWorkspace(); + const filePath = path.join(workspace, "invalid-diagnosis.ts"); + fs.writeFileSync(filePath, "const existing = true;\n", "utf8"); + + const sessionId = "invalid-llm-not-found-diagnosis"; + const snippet = await readSnippet(filePath, sessionId, workspace); + + const editResult = await handleEditTool( + { + snippet_id: snippet.id, + old_string: "const missing = true;", + new_string: "const missing = false;", + }, + createContext(sessionId, workspace, { + createOpenAIClient: () => ({ + client: { + chat: { + completions: { + create: async () => ({ + choices: [{ message: { content: "" } }], + }), + }, + }, + } as any, + model: "test-model", + thinkingEnabled: false, + }), + }) + ); + + assert.equal(editResult.ok, false); + assert.equal(editResult.error, "old_string not found in file."); + assert.equal(editResult.metadata?.closest_match, undefined); }); test("Edit allows outdated snippet matches but reports outdated snippet when no match is found", async () => { diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index eb133dae..e2190bb4 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -22,8 +22,6 @@ import { const MAX_CANDIDATE_COUNT = 5; const REPLACE_ALL_MATCH_THRESHOLD = 5; const SHORT_REPLACE_ALL_LENGTH = 40; -const MIN_FUZZY_SCORE = 0.8; -const CLOSEST_MATCH_CONTEXT_LINES = 2; const OUTDATED_SNIPPET_NOT_FOUND_ERROR = "old_string was not found in this snippet scope. The file has changed since this snippet was created. Read the file again before editing."; @@ -48,14 +46,6 @@ type MatchOccurrence = { endLine: number; }; -type ClosestMatch = { - text: string; - startLine: number; - endLine: number; - score: number; - strategy: "loose_escape" | "fuzzy_window"; -}; - type LooseEscapeMatch = MatchOccurrence & { text: string; score: number; @@ -274,19 +264,21 @@ export async function handleEditTool( }; } - const closestMatch = findClosestMatch(raw, oldString, scope, lineIndex); + const notFoundReason = await inferOldStringNotFoundReasonWithLLM( + raw, + lineIndex, + scope, + oldString, + newString, + context + ); return { ok: false, name: "edit", - error: "old_string not found in file.", - metadata: closestMatch - ? { - scope: formatScopeMetadata(scope), - closest_match: buildClosestMatchMetadata(context.sessionId, filePath, closestMatch), - } - : { - scope: formatScopeMetadata(scope), - }, + error: notFoundReason ? `old_string not found in file. ${notFoundReason}` : "old_string not found in file.", + metadata: { + scope: formatScopeMetadata(scope), + }, }; } @@ -586,24 +578,6 @@ function buildCandidateMetadata( }); } -function buildClosestMatchMetadata( - sessionId: string, - filePath: string, - closestMatch: ClosestMatch -): Record { - const preview = formatWithLineNumbers(closestMatch.text.split(/\r?\n/), closestMatch.startLine); - const snippet = createSnippet(sessionId, filePath, closestMatch.startLine, closestMatch.endLine, preview); - - return { - snippet_id: snippet?.id ?? null, - start_line: closestMatch.startLine, - end_line: closestMatch.endLine, - similarity: Number(closestMatch.score.toFixed(3)), - strategy: closestMatch.strategy, - preview, - }; -} - function formatScopeMetadata(scope: SearchScope): Record { return { file_path: scope.filePath, @@ -623,84 +597,6 @@ function formatWithLineNumbers(lines: string[], startLine: number): string { return lines.map((line, index) => `${String(startLine + index).padStart(6, " ")}\t${line}`).join("\n"); } -function findClosestMatch( - raw: string, - oldString: string, - scope: SearchScope, - lineIndex: LineIndex -): ClosestMatch | null { - const looseEscapeMatches = findLooseEscapeMatches(raw, oldString, scope); - if (looseEscapeMatches.length > 0) { - let bestLooseMatch: ClosestMatch | null = null; - for (const match of looseEscapeMatches) { - const candidate: ClosestMatch = { - text: match.text, - startLine: match.startLine, - endLine: match.endLine, - score: match.score, - strategy: "loose_escape", - }; - if (!bestLooseMatch || candidate.score > bestLooseMatch.score) { - bestLooseMatch = candidate; - } - } - - if (bestLooseMatch && bestLooseMatch.score >= MIN_FUZZY_SCORE) { - return expandClosestMatch(raw, lineIndex, scope, bestLooseMatch); - } - } - - const targetLineCount = Math.max(1, oldString.split(/\r?\n/).length); - const windowSizes = Array.from(new Set([Math.max(1, targetLineCount - 1), targetLineCount, targetLineCount + 1])); - const normalizedTarget = normalizeLooseText(oldString); - - let bestMatch: ClosestMatch | null = null; - for (let startLine = scope.startLine; startLine <= scope.endLine; startLine += 1) { - for (const windowSize of windowSizes) { - const endLine = startLine + windowSize - 1; - if (endLine > scope.endLine) { - continue; - } - - const candidateText = sliceLines(raw, lineIndex, startLine, endLine); - const score = similarityScore(normalizedTarget, normalizeLooseText(candidateText)); - if (score < MIN_FUZZY_SCORE) { - continue; - } - - const candidate: ClosestMatch = { - text: candidateText, - startLine, - endLine, - score, - strategy: "fuzzy_window", - }; - - if (!bestMatch || candidate.score > bestMatch.score) { - bestMatch = candidate; - } - } - } - - return bestMatch ? expandClosestMatch(raw, lineIndex, scope, bestMatch) : null; -} - -function expandClosestMatch( - raw: string, - lineIndex: LineIndex, - scope: SearchScope, - closestMatch: ClosestMatch -): ClosestMatch { - const startLine = clamp(closestMatch.startLine - CLOSEST_MATCH_CONTEXT_LINES, scope.startLine, scope.endLine); - const endLine = clamp(closestMatch.endLine + CLOSEST_MATCH_CONTEXT_LINES, startLine, scope.endLine); - return { - ...closestMatch, - text: sliceLines(raw, lineIndex, startLine, endLine), - startLine, - endLine, - }; -} - function buildLooseEscapeRegex(source: string): RegExp | null { if (!source) { return null; @@ -732,6 +628,91 @@ function buildLooseEscapeRegex(source: string): RegExp | null { return new RegExp(pattern, "g"); } +async function inferOldStringNotFoundReasonWithLLM( + raw: string, + lineIndex: LineIndex, + scope: SearchScope, + oldString: string, + newString: string, + context: ToolExecutionContext +): Promise { + const clientFactory = context.createOpenAIClient; + if (!clientFactory) { + return null; + } + + const { client, model, baseURL, thinkingEnabled, reasoningEffort } = clientFactory(); + if (!client) { + return null; + } + + const contextLineLimit = Math.max(1, oldString.split(/\r?\n/).length); + const snippetText = raw.slice(scope.startOffset, scope.endOffset); + const contentBeforeSnippet = getLinesBeforeScope(lineIndex, scope, contextLineLimit); + const contentAfterSnippet = getLinesAfterScope(lineIndex, scope, contextLineLimit); + + try { + const response = await client.chat.completions.create({ + model, + messages: [ + { + role: "system", + content: + "You diagnose failed file edits when old_string was not found. " + + "Return XML only using .... " + + "Be concise and specific. Explain the likely mismatch between old_string and the provided file context. " + + "Do not suggest unrelated changes.", + }, + { + role: "user", + content: + "\n" + + ` \n` + + ` \n` + + ` \n` + + ` \n` + + ` \n` + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + "", + }, + ], + ...buildThinkingRequestOptions(thinkingEnabled, baseURL, reasoningEffort), + }); + + return parseOldStringNotFoundReason(response.choices?.[0]?.message?.content ?? ""); + } catch { + return null; + } +} + +function getLinesBeforeScope(lineIndex: LineIndex, scope: SearchScope, lineLimit: number): string { + const startIndex = Math.max(0, scope.startLine - 1 - lineLimit); + const endIndex = Math.max(0, scope.startLine - 1); + return lineIndex.lines.slice(startIndex, endIndex).join("\n"); +} + +function getLinesAfterScope(lineIndex: LineIndex, scope: SearchScope, lineLimit: number): string { + const startIndex = Math.min(lineIndex.lines.length, scope.endLine); + const endIndex = Math.min(lineIndex.lines.length, startIndex + lineLimit); + return lineIndex.lines.slice(startIndex, endIndex).join("\n"); +} + +function parseOldStringNotFoundReason(content: string): string | null { + const trimmed = content.trim(); + if (!trimmed) { + return null; + } + + const normalized = trimmed.replace(/```(?:xml)?\s*([\s\S]*?)```/i, "$1").trim(); + const reasonMatch = normalized.match(/(?:|([\s\S]*?))<\/reason>/i); + const reason = (reasonMatch?.[1] ?? reasonMatch?.[2])?.trim(); + return reason || null; +} + async function correctEscapedStringsWithLLM( snippetText: string, oldString: string, @@ -884,9 +865,3 @@ function toBigrams(value: string): string[] { } return result; } - -function sliceLines(raw: string, lineIndex: LineIndex, startLine: number, endLine: number): string { - const startOffset = lineIndex.lineStarts[startLine]; - const endOffset = lineIndex.lineStarts[endLine + 1]; - return raw.slice(startOffset, endOffset); -} From dd8f7a2baa6da436064acc9e9560c5c3cdb5968f Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 29 May 2026 15:46:48 +0800 Subject: [PATCH 112/212] chore: update the prompt of inferOldStringNotFoundReasonWithLLM --- src/tools/edit-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index e2190bb4..b687c4e4 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -660,7 +660,7 @@ async function inferOldStringNotFoundReasonWithLLM( content: "You diagnose failed file edits when old_string was not found. " + "Return XML only using .... " + - "Be concise and specific. Explain the likely mismatch between old_string and the provided file context. " + + "Be concise and specific. Explain the likely mismatch between old_string and the content. " + "Do not suggest unrelated changes.", }, { From 0d68490ec675eac21e554c4ce63b2d7290eef3d9 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 29 May 2026 19:04:33 +0800 Subject: [PATCH 113/212] feat: add build-in skills karpathy-guidelines.md --- src/prompt.ts | 3 +- templates/skills/karpathy-guidelines.md | 67 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 templates/skills/karpathy-guidelines.md diff --git a/src/prompt.ts b/src/prompt.ts index 604041a7..2df96522 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -99,7 +99,8 @@ type PromptToolOptions = { webSearchEnabled?: boolean; }; -const DEFAULT_SKILL_TEMPLATES = ["agent-drift-guard.md", "plan-and-execute.md"]; +// const DEFAULT_SKILL_TEMPLATES = ["agent-drift-guard.md", "plan-and-execute.md"]; +const DEFAULT_SKILL_TEMPLATES = ["karpathy-guidelines.md", "plan-and-execute.md"]; function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): string { const toolsDir = path.join(extensionRoot, "templates", "tools"); diff --git a/templates/skills/karpathy-guidelines.md b/templates/skills/karpathy-guidelines.md new file mode 100644 index 00000000..c134d5e3 --- /dev/null +++ b/templates/skills/karpathy-guidelines.md @@ -0,0 +1,67 @@ +--- +name: karpathy-guidelines +description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria. +license: MIT +--- + +# Karpathy Guidelines + +Behavioral guidelines to reduce common LLM coding mistakes. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. \ No newline at end of file From d2770aa3f88c9bc3926f67da7e46314325cabab7 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Sat, 30 May 2026 00:08:04 +0800 Subject: [PATCH 114/212] feat: update default skill templates to include only karpathy-guidelines (Originally from: https://github.com/multica-ai/andrej-karpathy-skills Licensed under MIT) --- src/prompt.ts | 3 +-- src/tests/prompt.test.ts | 9 +++------ src/tests/session.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/prompt.ts b/src/prompt.ts index 2df96522..4bd1288d 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -99,8 +99,7 @@ type PromptToolOptions = { webSearchEnabled?: boolean; }; -// const DEFAULT_SKILL_TEMPLATES = ["agent-drift-guard.md", "plan-and-execute.md"]; -const DEFAULT_SKILL_TEMPLATES = ["karpathy-guidelines.md", "plan-and-execute.md"]; +const DEFAULT_SKILL_TEMPLATES = ["karpathy-guidelines.md"]; function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): string { const toolsDir = path.join(extensionRoot, "templates", "tools"); diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts index 953de7ca..bda03049 100644 --- a/src/tests/prompt.test.ts +++ b/src/tests/prompt.test.ts @@ -49,14 +49,11 @@ test("getSystemPrompt does not include runtime context", () => { assert.equal(prompt.includes('"root path": "/tmp/project"'), false); }); -test("getDefaultSkillPrompt loads default skill templates in order", () => { +test("getDefaultSkillPrompt loads the default skill template", () => { const prompt = getDefaultSkillPrompt(); - const agentDriftIndex = prompt.indexOf(""); - const planIndex = prompt.indexOf(""); - assert.notEqual(agentDriftIndex, -1); - assert.notEqual(planIndex, -1); - assert.equal(agentDriftIndex < planIndex, true); + assert.equal(prompt.includes(""), true); + assert.equal(prompt.includes("# Karpathy Guidelines"), true); assert.equal(prompt.includes("Use the skill documents below to assist the user:"), true); assert.equal(prompt.includes('path="templates/skills/'), false); }); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index b3cbe529..06808ed2 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -702,8 +702,8 @@ test("createSession appends default system prompts in prefix-cache-friendly orde assert.match(systemContents[0] ?? "", /# Available Tools/); assert.doesNotMatch(systemContents[0] ?? "", /# Local Workspace Environment/); assert.doesNotMatch(systemContents[0] ?? "", /当前LLM模型为test-model/); - assert.match(systemContents[1] ?? "", //); - assert.match(systemContents[1] ?? "", //); + assert.match(systemContents[1] ?? "", //); + assert.match(systemContents[1] ?? "", /# Karpathy Guidelines/); assert.doesNotMatch(systemContents[1] ?? "", /path="templates\/skills\//); assert.doesNotMatch(systemContents[1] ?? "", /当前LLM模型为test-model/); assert.match(systemContents[2] ?? "", /# Local Workspace Environment/); From 41fc95b19d4b950fe55db09361f25d176a6e9dc8 Mon Sep 17 00:00:00 2001 From: fym998 <61316972+fym998@users.noreply.github.com> Date: Fri, 29 May 2026 23:58:31 +0800 Subject: [PATCH 115/212] chore(deps): update ink-gradient to 4.0.1 --- package-lock.json | 11 ++++++----- package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4044a2d..a8bcf74a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "gray-matter": "^4.0.3", "ignore": "^7.0.5", "ink": "^7.0.4", - "ink-gradient": "^4.0.0", + "ink-gradient": "^4.0.1", "openai": "^6.35.0", "react": "^19.2.5", "undici": "^7.25.0", @@ -2503,9 +2503,9 @@ } }, "node_modules/ink-gradient": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/ink-gradient/-/ink-gradient-4.0.0.tgz", - "integrity": "sha512-Yx227CStr4DaXVkRAQPbBufSUTqe4a4FLOPVoypXZyae5h3A5jWyqZpTmAIbm7iiiqNYCkKIFBUPJM6nSICfxA==", + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/ink-gradient/-/ink-gradient-4.0.1.tgz", + "integrity": "sha512-0ckdiM84zkfCdnTtcnq4BS3egIhUPPDoCqSx/7NUFsAVooBbdRuGnnWpk0fuaOTqU6rlZRh9F4LN1UI8fxd81Q==", "license": "MIT", "dependencies": { "@types/gradient-string": "^1.1.6", @@ -2519,7 +2519,8 @@ "url": "https://github.com/sponsors/sindresorhus" }, "peerDependencies": { - "ink": ">=6" + "ink": ">=6", + "react": ">=19.2.0" } }, "node_modules/is-extendable": { diff --git a/package.json b/package.json index 565ba554..169c13db 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "gray-matter": "^4.0.3", "ignore": "^7.0.5", "ink": "^7.0.4", - "ink-gradient": "^4.0.0", + "ink-gradient": "^4.0.1", "openai": "^6.35.0", "react": "^19.2.5", "undici": "^7.25.0", From fe0014546852f17eedccd04b723a8019f6ea9b17 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Sat, 30 May 2026 09:49:39 +0800 Subject: [PATCH 116/212] feat: update AGENTS.md --- .deepcode/AGENTS.md | 23 ++++++++++++++--------- AGENTS.md | 35 ----------------------------------- 2 files changed, 14 insertions(+), 44 deletions(-) delete mode 100644 AGENTS.md diff --git a/.deepcode/AGENTS.md b/.deepcode/AGENTS.md index bdd787ab..167d5a68 100644 --- a/.deepcode/AGENTS.md +++ b/.deepcode/AGENTS.md @@ -22,6 +22,7 @@ src/ │ ├── process-tree.ts # Process tree construction and orphan detection │ ├── shell-utils.ts # Shell path resolution (Git Bash, zsh, bash) │ ├── state.ts # In-memory file state and snippet tracking +│ ├── telemetry.ts # Usage telemetry collection and reporting │ ├── update-check.ts # Latest-version check against npm registry │ └── validate.ts # Tool validation runtime helpers (was runtime.ts) ├── mcp/ @@ -37,20 +38,23 @@ src/ │ ├── web-search-handler.ts # Web search via natural language queries │ └── ask-user-question-handler.ts # Interactive user prompts with options ├── ui/ -│ ├── components/ # Reusable Ink components (MessageView, DropdownMenu, ModelsDropdown, SkillsDropdown, etc.) +│ ├── components/ # Reusable Ink components (MessageView, DropdownMenu, ModelsDropdown, SkillsDropdown, FileMentionMenu, RawModelDropdown, etc.) │ ├── contexts/ # React contexts (AppContext, RawModeContext) │ ├── hooks/ # Custom hooks (cursor, useHistoryNavigation, usePasteHandling, useTerminalInput) -│ ├── views/ # Top-level screen components (App, PromptInput, SessionList, PermissionPrompt, ProcessStdoutView, etc.) -│ ├── core/ # Core UI logic (file-mentions, slash-commands, prompt-buffer, thinking-state, etc.) +│ ├── views/ # Top-level screen components (App, PromptInput, SessionList, PermissionPrompt, ProcessStdoutView, WelcomeScreen, UndoSelector, etc.) +│ ├── core/ # Core UI logic (file-mentions, slash-commands, prompt-buffer, thinking-state, clipboard, prompt-undo-redo, etc.) │ ├── utils/ # Shared utility helpers +│ ├── ascii-art.ts # ASCII art banner for welcome screen +│ ├── exit-summary.ts # Session exit summary and cost reporting │ ├── index.ts # UI module barrel exports │ └── constants.ts # UI-wide constants -├── tests/ # One *.test.ts per source module, plus run-tests.mjs +├── tests/ # Test files per source module, plus run-tests.mjs templates/ ├── tools/ # Tool descriptions fed to the LLM -├── skills/ # Built-in skill definitions (agent-drift-guard, plan-and-execute) +├── skills/ # Built-in skill definitions (agent-drift-guard, plan-and-execute, karpathy-guidelines) └── prompts/ # EJS templates (e.g., init_command.md.ejs) docs/ # User-facing documentation (configuration, MCP, notify, permissions) +resources/ # Static assets (intro screenshots) dist/ # Bundled CLI output (gitignored) ``` @@ -66,8 +70,8 @@ dist/ # Bundled CLI output (gitignored) | `npm run check` | Runs typecheck + lint + format:check together | | `npm run bundle` | esbuild bundles `src/cli.tsx` → `dist/cli.js` (ESM, Node 18) | | `npm run build` | `check` + `bundle` + chmod 755 — full CI gate before publish | -| `npm test` | Runs all tests via `tsx --test src/tests/*.test.ts` | -| `npm run test:single -- ` | Run a single test file (e.g., `npm run test:single -- src/tests/session.test.ts`) | +| `npm test` | Runs all tests via `node src/tests/run-tests.mjs` | +| `npm run test:single -- ` | Run a single test file via `tsx --test` (e.g., `npm run test:single -- src/tests/session.test.ts`) | Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundle`). @@ -90,7 +94,7 @@ Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundl - **Framework**: Node.js native test runner (`node:test`) with `tsx` for TypeScript - **Assertions**: `node:assert/strict` -- **Coverage**: Target meaningful unit tests for core logic (session management, tool handlers, settings resolution, prompt buffer, permissions, MCP client). Test files are in `src/tests/` matching the source module name. +- **Coverage**: Target meaningful unit tests for core logic (session management, tool handlers, settings resolution, prompt buffer, permissions, MCP client, telemetry). Test files are in `src/tests/` matching the source module name. - **Test naming**: `describe`/`test` blocks with descriptive names. Example: `test("SessionManager preserves structured system content when building OpenAI messages", ...)` - **Relaxed lint rules**: Test files allow `any` and unused vars. - Run all tests with `npm test` before submitting a PR. A cross-platform test runner is available at `src/tests/run-tests.mjs`. @@ -104,6 +108,7 @@ Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundl - `chore:` — tooling, deps, hooks (e.g., `chore: add husky + lint-staged`) - `refactor:` — code restructuring (e.g., `refactor(ui): optimize App hooks`) - `style:` — formatting-only changes (e.g., `style: adjust the tree structure symbols`) +- `test:` — adding or updating tests (e.g., `test: add telemetry module unit tests`) - `docs:` — documentation (e.g., `docs: add MCP configuration guide`) **Pull requests** should include: @@ -133,4 +138,4 @@ A **file history system** (`src/common/file-history.ts`) provides undo/checkpoin - **AGENTS.md loading**: The CLI loads agent instructions from `./AGENTS.md`, `./.deepcode/AGENTS.md`, or `~/.deepcode/AGENTS.md` (first found wins). Write project-specific guidance for the LLM in any of these. - **Skills**: Place skill definitions in `~/.agents/skills//SKILL.md` (user-level) or `./.agents/skills//SKILL.md` (project-level). Legacy path `./.deepcode/skills/` is also supported. Each SKILL.md uses YAML frontmatter with `name` and `description` fields. -- **Built-in skills**: `agent-drift-guard` (detects and corrects execution drift) and `plan-and-execute` (structured task planning with progress tracking). Both are defined in `templates/skills/` and always injected into every session. +- **Built-in skills**: `agent-drift-guard` (detects and corrects execution drift), `plan-and-execute` (structured task planning with progress tracking), and `karpathy-guidelines` (behavioral guidelines to reduce common LLM coding mistakes). All three are defined in `templates/skills/` and always injected into every session. diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 44a3a244..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,35 +0,0 @@ -# Repository Guidelines - -## Project Structure & Module Organization - -- `src/` contains the TypeScript CLI implementation, with tool handlers in `src/tools/`, MCP integration in `src/mcp/`, UI components in `src/ui/`, and shared helpers in `src/common/`. -- `src/tests/` contains Node test files named `*.test.ts`. -- `templates/` contains runtime prompt assets: `templates/prompts/` for EJS prompt templates and `templates/tools/` for tool instruction Markdown loaded into the system prompt. -- `docs/` is reserved for user-facing documentation such as configuration and MCP guides. -- `resources/` stores static images used by the documentation or UI. - -## Build, Test, and Development Commands - -- `npm test` runs all test files with `tsx --test`. -- `npm run test:single -- src/tests/.test.ts` runs one test file. -- `npm run typecheck` verifies TypeScript types without emitting files. -- `npm run lint` checks ESLint rules for `src/`. -- `npm run build` runs checks, bundles `src/cli.tsx` to `dist/cli.js`, and marks the bundle executable. - -## Coding Style & Naming Conventions - -- Use TypeScript ES modules and keep imports explicit. -- Prefer small, focused functions; keep filesystem path construction centralized when a path is reused. -- Use two-space indentation and Prettier-compatible formatting. -- Respond in standard technical English. Avoid nonstandard phrasing and corporate jargon. - -## Testing Guidelines - -- Add or update tests in `src/tests/` when changing command behavior, prompt rendering, session flow, tools, or settings. -- Prefer Node's built-in `node:test` and `node:assert/strict` APIs, matching the existing tests. -- Keep tests deterministic by using temporary directories and mocked network calls where needed. - -## Commit & Pull Request Guidelines - -- Keep commits focused on a single change and use concise, imperative commit messages. -- In pull requests, describe the behavior change, list verification commands, and note any packaging or template path changes. From 0e2a6ce7a27ebbea039efb06faeb2370dbec236b Mon Sep 17 00:00:00 2001 From: michaehuang Date: Sat, 30 May 2026 14:59:58 +0800 Subject: [PATCH 117/212] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0API=20Key=20?= =?UTF-8?q?not=20found=E6=97=B6=E7=9A=84=E6=96=87=E6=9C=AC=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/session.ts b/src/session.ts index 358789e4..57108dee 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1185,13 +1185,13 @@ ${skillMd} this.updateSessionEntry(sessionId, (entry) => ({ ...entry, status: "failed", - failReason: "OpenAI API key not found", + failReason: "API key not found", updateTime: now, })); this.onAssistantMessage( this.buildAssistantMessage( sessionId, - "OpenAI API key not found. Please configure ~/.deepcode/settings.json or ./.deepcode/settings.json.", + "API key not found. Please configure ~/.deepcode/settings.json or ./.deepcode/settings.json.", null ), false From 894b2932e75818563aa7a3d4374f71477a42d727 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 1 Jun 2026 17:05:35 +0800 Subject: [PATCH 118/212] 0.1.27 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8bcf74a..d354378d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.26", + "version": "0.1.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.26", + "version": "0.1.27", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index 169c13db..23875a4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.26", + "version": "0.1.27", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From 66e34d392e2ecb7cecaec8ae46770dfc557aff87 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 2 Jun 2026 09:50:22 +0800 Subject: [PATCH 119/212] feat: update the Bash tool param to render on one line --- src/tests/message-view.test.ts | 34 ++++++++++++++++++++++ src/ui/components/MessageView/utils.ts | 40 +++++++++++++++----------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/tests/message-view.test.ts b/src/tests/message-view.test.ts index b806dbd1..ba1a9152 100644 --- a/src/tests/message-view.test.ts +++ b/src/tests/message-view.test.ts @@ -3,6 +3,8 @@ import assert from "node:assert/strict"; import { parseDiffPreview } from "../ui"; import { buildThinkingSummary, + formatBashStatusParams, + formatToolStatusParams, renderMessageToStdout, getUpdatePlanPreviewLines, parseToolPayload, @@ -60,6 +62,26 @@ test("MessageView shows full reasoning content in Normal/Raw mode", () => { ); }); +test("formatBashStatusParams compacts multi-line commands and keeps the final description", () => { + assert.equal( + formatBashStatusParams('python3 -c "\nprint(1)\nprint(2)\n" # Run inline script'), + 'python3 -c " ... " # Run inline script' + ); +}); + +test("formatToolStatusParams preserves compacted Bash params but truncates other tools", () => { + assert.equal( + formatToolStatusParams({ + name: "bash", + params: "cat <<'EOF'\nhello\nEOF # Print heredoc", + ok: true, + metadata: null, + }), + "cat <<'EOF' ... EOF # Print heredoc" + ); + assert.equal(formatToolStatusParams({ name: "read", params: "first\nsecond", ok: true, metadata: null }), "first"); +}); + // --- renderMessageToStdout tests --- function makeSessionMessage(overrides: Partial & Pick): SessionMessage { @@ -139,6 +161,18 @@ test("renderMessageToStdout renders tool messages with resultMd output", () => { assert.ok(output.includes("line 1")); }); +test("renderMessageToStdout compacts multi-line Bash params", () => { + const payload = JSON.stringify({ name: "bash", ok: true }); + const msg = makeSessionMessage({ + role: "tool", + content: payload, + meta: { paramsMd: 'python3 -c "\nprint(1)\nprint(2)\n" # Run inline script' }, + }); + const output = renderMessageToStdout(msg, RawMode.Raw); + assert.ok(output.includes('python3 -c " ... " # Run inline script')); + assert.ok(!output.includes("print(1)")); +}); + test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview and resultMd", () => { const payload = JSON.stringify({ name: "UpdatePlan", diff --git a/src/ui/components/MessageView/utils.ts b/src/ui/components/MessageView/utils.ts index af5391d8..91ae64be 100644 --- a/src/ui/components/MessageView/utils.ts +++ b/src/ui/components/MessageView/utils.ts @@ -54,10 +54,28 @@ export function buildThinkingSummary(content: string, messageParams: unknown | n return ""; } -/** Formats a tool's parameters for status display, preserving full bash commands but truncating others. */ +/** Formats multi-line Bash params as first line, a placeholder, and the final line. */ +export function formatBashStatusParams(params: string): string { + const value = params.trim(); + if (!value) { + return ""; + } + + const lines = value.split(/\r?\n/); + if (lines.length <= 1) { + return value; + } + + return `${lines[0]} ... ${lines[lines.length - 1].trimStart()}`; +} + +/** Formats a tool's parameters for status display, compacting multi-line Bash commands and truncating others. */ export function formatToolStatusParams(summary: ToolSummary): string { + if (summary.name.toLowerCase() === "bash") { + return formatBashStatusParams(summary.params); + } const params = firstNonEmptyLine(summary.params); - return summary.name.toLowerCase() === "bash" ? params : truncate(params, 120); + return truncate(params, 120); } /** Builds a structured summary (name, params, ok, metadata) from a tool session message. */ @@ -226,25 +244,13 @@ export function renderMessageToStdout(message: SessionMessage, mode: RawMode): s } if (message.role === "tool") { - const payload = parseToolPayload(message.content); - const metaFunctionName = - message.meta?.function && typeof (message.meta.function as { name?: unknown }).name === "string" - ? (message.meta.function as { name: string }).name - : null; - const name = payload.name || metaFunctionName || "tool"; - const metaParams = typeof message.meta?.paramsMd === "string" ? message.meta.paramsMd.trim() : ""; - const params = name.toLowerCase() === "bash" ? metaParams : truncate(metaParams, 120); - const statusLine = `${chalk("✧")} ${chalk(formatStatusName(name))}${params ? ` ${chalk(params)}` : ""}`; + const summary = buildToolSummary(message); + const params = formatToolStatusParams(summary); + const statusLine = `${chalk("✧")} ${chalk(formatStatusName(summary.name))}${params ? ` ${chalk(params)}` : ""}`; const metaResultMd = typeof message.meta?.resultMd === "string" ? message.meta.resultMd.trim() : ""; const result = metaResultMd ? `\n${chalk.dim(" └ Result")}\n${metaResultMd}` : ""; - const summary: ToolSummary = { - name, - params, - ok: payload.ok !== false, - metadata: payload.metadata, - }; const planLines = getUpdatePlanPreviewLines(summary); if (planLines.length > 0) { const planText = planLines.map((line) => ` ${line}`).join("\n"); From 63de6fb9b7c52a6537ee9809f3f95f81e4c1efae Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 2 Jun 2026 10:57:02 +0800 Subject: [PATCH 120/212] feat: implemented Bash run_in_background support --- src/prompt.ts | 5 ++ src/session.ts | 41 ++++++++++ src/tests/prompt.test.ts | 8 ++ src/tests/tool-handlers.test.ts | 74 +++++++++++++++++- src/tools/bash-handler.ts | 133 ++++++++++++++++++++++++++++++++ src/tools/executor.ts | 18 +++++ src/ui/views/App.tsx | 12 ++- templates/tools/bash.md | 6 ++ 8 files changed, 293 insertions(+), 4 deletions(-) diff --git a/src/prompt.ts b/src/prompt.ts index 4bd1288d..7b98d457 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -351,6 +351,11 @@ export function getTools(_options: PromptToolOptions = {}, externalTools: ToolDe }, uniqueItems: true, }, + run_in_background: { + type: "boolean", + description: + "Set to true to run the command in the background. Use this only when you need to perform a blocking task and do not need the result immediately.", + }, }, required: ["command", "sideEffects"], additionalProperties: false, diff --git a/src/session.ts b/src/session.ts index 57108dee..1f3cca7b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2172,6 +2172,7 @@ ${skillMd} onProcessExit: (pid) => this.removeSessionProcess(sessionId, pid), onProcessStdout: (pid, chunk) => this.onProcessStdout?.(Number(pid), chunk), onProcessTimeoutControl: (pid, control) => this.setSessionProcessTimeoutControl(sessionId, pid, control), + onBackgroundProcessComplete: (completion) => this.addBackgroundProcessCompletionMessage(sessionId, completion), onBeforeFileMutation: (filePath) => this.prepareFileMutationCheckpoint(sessionId, filePath), onAfterFileMutation: (filePath) => this.recordFileMutationCheckpoint(sessionId, filePath), shouldStop: () => this.isInterrupted(sessionId), @@ -2677,6 +2678,46 @@ ${skillMd} }); } + private addBackgroundProcessCompletionMessage( + sessionId: string, + completion: { + command: string; + outputPath: string; + ok: boolean; + exitCode: number | null; + signal: string | null; + error?: string; + completedAtMs: number; + startedAtMs: number; + } + ): void { + const status = completion.ok ? "completed" : "failed"; + const exitText = + completion.exitCode !== null + ? `exit code ${completion.exitCode}` + : completion.signal + ? `signal ${completion.signal}` + : completion.error || "unknown status"; + const durationMs = Math.max(0, completion.completedAtMs - completion.startedAtMs); + const content = + `Background command "${completion.command}" ${status} with ${exitText} ` + + `after ${this.formatBackgroundDuration(durationMs)}. Output: ${completion.outputPath}`; + this.addSessionSystemMessage(sessionId, content, true); + } + + private formatBackgroundDuration(durationMs: number): string { + if (durationMs < 1000) { + return `${durationMs}ms`; + } + const seconds = Math.round(durationMs / 1000); + if (seconds < 60) { + return `${seconds}s`; + } + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`; + } + private removeSessionProcess(sessionId: string, processId: string | number): void { const now = new Date().toISOString(); this.processTimeoutControls.delete(this.getProcessControlKey(sessionId, processId)); diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts index bda03049..52024a32 100644 --- a/src/tests/prompt.test.ts +++ b/src/tests/prompt.test.ts @@ -30,6 +30,8 @@ test("getTools requires bash sideEffects permission scopes", () => { assert.equal(sideEffects.type, "array"); assert.equal(sideEffects.items?.enum?.includes("write-out-cwd"), true); assert.equal(sideEffects.items?.enum?.includes("unknown"), true); + const runInBackground = tool.function.parameters.properties.run_in_background as { type?: unknown }; + assert.equal(runInBackground.type, "boolean"); }); test("getSystemPrompt always includes WebSearch docs", () => { @@ -43,6 +45,12 @@ test("getSystemPrompt includes UpdatePlan docs", () => { assert.equal(prompt.includes("The `plan` argument is a markdown string, not an array of step objects."), true); }); +test("getSystemPrompt includes Bash background guidance", () => { + const prompt = getSystemPrompt("/tmp/project"); + assert.equal(prompt.includes("run_in_background: true"), true); + assert.equal(prompt.includes("do not add `&`"), true); +}); + test("getSystemPrompt does not include runtime context", () => { const prompt = getSystemPrompt("/tmp/project"); assert.equal(prompt.includes("# Local Workspace Environment"), false); diff --git a/src/tests/tool-handlers.test.ts b/src/tests/tool-handlers.test.ts index aefef2ff..d070d3e0 100644 --- a/src/tests/tool-handlers.test.ts +++ b/src/tests/tool-handlers.test.ts @@ -4,7 +4,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { setTimeout as delay } from "node:timers/promises"; -import type { ProcessTimeoutControl, ToolExecutionContext } from "../tools/executor"; +import type { BackgroundProcessCompletion, ProcessTimeoutControl, ToolExecutionContext } from "../tools/executor"; import { handleBashTool } from "../tools/bash-handler"; import { handleEditTool } from "../tools/edit-handler"; import { handleReadTool } from "../tools/read-handler"; @@ -104,6 +104,78 @@ test("Bash timeout control can extend the active command deadline", async () => assert.equal(result.metadata?.timeoutMs, 1000); }); +test("Bash can run commands in the background and report completion output", async () => { + const workspace = createTempWorkspace(); + let completion: BackgroundProcessCompletion | null = null; + const starts: Array = []; + const exits: Array = []; + const startedAt = Date.now(); + + const result = await handleBashTool( + { + command: "printf 'start\\n'; sleep 0.2; printf 'done\\n'", + run_in_background: true, + }, + createContext("bash-background", workspace, { + bashTimeoutMs: 10, + bashMinTimeoutMs: 1, + onProcessStart: (pid) => starts.push(pid), + onProcessExit: (pid) => exits.push(pid), + onBackgroundProcessComplete: (event) => { + completion = event; + }, + }) + ); + + assert.equal(result.ok, true); + assert.equal(result.metadata?.runInBackground, true); + assert.equal(typeof result.metadata?.backgroundTaskId, "string"); + assert.equal(typeof result.metadata?.outputPath, "string"); + assert.ok(Date.now() - startedAt < 500); + assert.equal(starts.length, 1); + + await waitFor(() => completion !== null, 2000); + + assert.ok(completion); + const done = completion as BackgroundProcessCompletion; + assert.equal(done.ok, true); + assert.equal(done.exitCode, 0); + assert.equal(exits.length, 1); + const outputPath = done.outputPath; + const output = fs.readFileSync(outputPath, "utf8"); + assert.match(output, /start/); + assert.match(output, /done/); + assert.doesNotMatch(output, /__DEEPCODE_PWD__/); +}); + +test("Bash background completion reports failed exit codes", async () => { + const workspace = createTempWorkspace(); + let completion: BackgroundProcessCompletion | null = null; + + const result = await handleBashTool( + { + command: "printf 'bad\\n'; exit 7", + run_in_background: true, + }, + createContext("bash-background-failure", workspace, { + onBackgroundProcessComplete: (event) => { + completion = event; + }, + }) + ); + + assert.equal(result.ok, true); + await waitFor(() => completion !== null, 2000); + + assert.ok(completion); + const done = completion as BackgroundProcessCompletion; + assert.equal(done.ok, false); + assert.equal(done.exitCode, 7); + assert.match(done.error ?? "", /exit code 7/); + const output = fs.readFileSync(done.outputPath, "utf8"); + assert.match(output, /bad/); +}); + test("UpdatePlan accepts a markdown task list string", async () => { const workspace = createTempWorkspace(); const plan = ["## Task List", "", "- [>] Inspect current behavior", "- [ ] Implement UpdatePlan"].join("\n"); diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts index 7d9a3736..ae42a354 100644 --- a/src/tools/bash-handler.ts +++ b/src/tools/bash-handler.ts @@ -1,4 +1,8 @@ import { spawn } from "child_process"; +import { randomUUID } from "crypto"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/bash-timeout"; import { killProcessTree } from "../common/process-tree"; import type { ProcessTimeoutControl, ProcessTimeoutInfo, ToolExecutionContext, ToolExecutionResult } from "./executor"; @@ -13,6 +17,7 @@ import { const MAX_OUTPUT_CHARS = 30000; const MAX_CAPTURE_CHARS = 10 * 1024 * 1024; +const BACKGROUND_OUTPUT_DIR = path.join(os.tmpdir(), "deepcode-background"); const sessionWorkingDirs = new Map(); export function clearSessionWorkingDir(sessionId: string): void { @@ -51,6 +56,11 @@ export async function handleBashTool( const startCwd = getSessionCwd(context.sessionId, context.projectRoot); const { shellPath, shellArgs, marker } = buildShellCommand(command); + const runInBackground = isTrue(args.run_in_background); + + if (runInBackground) { + return startBackgroundShellCommand(shellPath, shellArgs, startCwd, command, marker, context); + } const execution = await executeShellCommand(shellPath, shellArgs, startCwd, command, context); const result = buildToolCommandResult( @@ -75,6 +85,10 @@ export async function handleBashTool( return formatResult(result, "bash"); } +function isTrue(value: unknown): boolean { + return value === true || value === "true"; +} + function getSessionCwd(sessionId: string, fallback: string): string { return sessionWorkingDirs.get(sessionId) ?? fallback; } @@ -235,6 +249,125 @@ async function executeShellCommand( }); } +function startBackgroundShellCommand( + shellPath: string, + shellArgs: string[], + cwd: string, + command: string, + marker: string, + context: ToolExecutionContext +): ToolExecutionResult { + fs.mkdirSync(BACKGROUND_OUTPUT_DIR, { recursive: true }); + const taskId = `bash-${randomUUID()}`; + const outputPath = path.join(BACKGROUND_OUTPUT_DIR, `${taskId}.log`); + const startedAtMs = Date.now(); + const detached = process.platform !== "win32"; + const configuredEnv = context.createOpenAIClient?.().env ?? {}; + const child = spawn(shellPath, shellArgs, { + cwd, + env: buildShellEnv(shellPath, configuredEnv), + detached, + windowsHide: true, + stdio: ["ignore", "pipe", "pipe"], + }); + const pid = child.pid; + const processId = typeof pid === "number" ? pid : -1; + + let stdout = ""; + let stderr = ""; + let error: string | undefined; + + const appendOutputFile = (chunk: string | Buffer) => { + try { + fs.appendFileSync(outputPath, chunk); + } catch { + // Keep the background process running even if temp-file writes fail. + } + }; + + if (typeof pid === "number") { + context.onProcessStart?.(pid, command); + } + + child.stdout?.on("data", (chunk: string | Buffer) => { + stdout = appendChunk(stdout, chunk); + appendOutputFile(chunk); + const text = typeof chunk === "string" ? chunk : chunk.toString("utf8"); + if (typeof pid === "number") { + context.onProcessStdout?.(pid, text); + } + }); + child.stderr?.on("data", (chunk: string | Buffer) => { + stderr = appendChunk(stderr, chunk); + appendOutputFile(chunk); + const text = typeof chunk === "string" ? chunk : chunk.toString("utf8"); + if (typeof pid === "number") { + context.onProcessStdout?.(pid, text); + } + }); + + child.on("error", (spawnError) => { + error = spawnError.message; + }); + + child.on("close", (code, signal) => { + const markerResult = stripMarker(stdout, marker); + const finalOutput = joinOutput(markerResult.output, stderr); + const result = buildToolCommandResult( + stdout, + stderr, + marker, + typeof code === "number" ? code : null, + signal ?? null, + shellPath, + cwd + ); + updateSessionCwd(context.sessionId, cwd, result.cwd); + writeFinalBackgroundOutput(outputPath, finalOutput); + if (typeof pid === "number") { + context.onProcessExit?.(pid); + } + const ok = !error && result.exitCode === 0 && result.signal === null; + context.onBackgroundProcessComplete?.({ + taskId, + processId, + command, + outputPath, + ok, + exitCode: result.exitCode, + signal: result.signal, + error: ok ? undefined : buildErrorMessage(result.exitCode, result.signal, error), + cwd: result.cwd, + shellPath, + startedAtMs, + completedAtMs: Date.now(), + }); + }); + + return { + ok: true, + name: "bash", + output: `Command running in background with ID: ${taskId}. Output is being written to: ${outputPath}`, + metadata: { + backgroundTaskId: taskId, + processId: typeof pid === "number" ? pid : null, + outputPath, + cwd, + shellPath, + startCwd: cwd, + runInBackground: true, + }, + }; +} + +function writeFinalBackgroundOutput(outputPath: string, output: string | undefined): void { + try { + fs.writeFileSync(outputPath, output ?? "", "utf8"); + } catch { + // Ignore notification/output persistence failures; the tool result already returned. + } +} + function appendChunk(existing: string, chunk: string | Buffer): string { if (existing.length >= MAX_CAPTURE_CHARS) { return existing; diff --git a/src/tools/executor.ts b/src/tools/executor.ts index 155c8724..60b18882 100644 --- a/src/tools/executor.ts +++ b/src/tools/executor.ts @@ -41,6 +41,7 @@ export type ToolExecutionContext = { onProcessExit?: (processId: string | number) => void; onProcessStdout?: (processId: string | number, chunk: string) => void; onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; + onBackgroundProcessComplete?: (completion: BackgroundProcessCompletion) => void; onBeforeFileMutation?: (filePath: string) => void; onAfterFileMutation?: (filePath: string) => void; bashTimeoutMs?: number; @@ -52,11 +53,27 @@ export type ToolExecutionHooks = { onProcessExit?: (processId: string | number) => void; onProcessStdout?: (processId: string | number, chunk: string) => void; onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; + onBackgroundProcessComplete?: (completion: BackgroundProcessCompletion) => void; onBeforeFileMutation?: (filePath: string) => void; onAfterFileMutation?: (filePath: string) => void; shouldStop?: () => boolean; }; +export type BackgroundProcessCompletion = { + taskId: string; + processId: number; + command: string; + outputPath: string; + ok: boolean; + exitCode: number | null; + signal: string | null; + error?: string; + cwd: string | null; + shellPath: string; + startedAtMs: number; + completedAtMs: number; +}; + export type ProcessTimeoutInfo = { timeoutMs: number; startedAtMs: number; @@ -230,6 +247,7 @@ export class ToolExecutor { onProcessExit: hooks?.onProcessExit, onProcessStdout: hooks?.onProcessStdout, onProcessTimeoutControl: hooks?.onProcessTimeoutControl, + onBackgroundProcessComplete: hooks?.onBackgroundProcessComplete, onBeforeFileMutation: hooks?.onBeforeFileMutation, onAfterFileMutation: hooks?.onAfterFileMutation, }); diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index bef803e3..f07da6f5 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -328,9 +328,12 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl setBusy(true); setErrorLine(null); - setRunningProcesses(null); + const activeProcesses = activeSessionId ? (sessionManager.getSession(activeSessionId)?.processes ?? null) : null; + setRunningProcesses(activeProcesses); setShowProcessStdout(false); - processStdoutRef.current.clear(); + if (!activeProcesses || activeProcesses.size === 0) { + processStdoutRef.current.clear(); + } try { await sessionManager.handleUserPrompt(prompt); if (permissionReply) { @@ -344,7 +347,10 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl } finally { setBusy(false); setStreamProgress(null); - setRunningProcesses(null); + const finalActiveSessionId = sessionManager.getActiveSessionId(); + setRunningProcesses( + finalActiveSessionId ? (sessionManager.getSession(finalActiveSessionId)?.processes ?? null) : null + ); } }, [ diff --git a/templates/tools/bash.md b/templates/tools/bash.md index 83027d3f..db711645 100644 --- a/templates/tools/bash.md +++ b/templates/tools/bash.md @@ -29,6 +29,8 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. - The sideEffects argument is required. Declare the minimum permission scopes the command may need. + - You can use `run_in_background: true` to run a command in the background. Only use this if you need to perform a blocking task, like running a server for the upcoming test scripts. + - When using `run_in_background`, do not add `&` to the command. Output is written to a log file. - Use `sideEffects: []` only for commands that do not read, write, delete, query Git history, mutate Git history, or access the network, such as `date` or `node --version`. - Use `*-out-cwd` when the command accesses paths outside the current workspace. For example, `cat /etc/hosts` requires `["read-out-cwd"]`. - Use `query-git-log` for commands such as `git log`, `git show HEAD`, `git blame`, or history diffs. Use `mutate-git-log` for commands such as `git commit`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, or `git tag`. @@ -85,6 +87,10 @@ Usage notes: ] }, "uniqueItems": true + }, + "run_in_background": { + "description": "Set to true to run the command in the background. Use this only when you do not need the result immediately and can wait for a completion notification.", + "type": "boolean" } }, "required": [ From d88fe1c79631f45b25a03deee295256e205ef141 Mon Sep 17 00:00:00 2001 From: lellansin Date: Fri, 29 May 2026 15:06:29 +0800 Subject: [PATCH 121/212] refactor: extract OpenAI message converter from SessionManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move buildOpenAIMessages, sessionMessageToOpenAIMessage, pairToolMessages, getTrailingPendingToolCallMessage, and related helpers (~240 lines) into a dedicated OpenAIMessageConverter class under src/common/. SessionManager now delegates to the converter while retaining a deprecated buildOpenAIMessages pass-through for test compatibility. Net reduction: 2884 → 2644 lines in session.ts --- src/common/openai-message-converter.ts | 278 +++++++++++++++++++++++ src/session.ts | 291 +++---------------------- 2 files changed, 309 insertions(+), 260 deletions(-) create mode 100644 src/common/openai-message-converter.ts diff --git a/src/common/openai-message-converter.ts b/src/common/openai-message-converter.ts new file mode 100644 index 00000000..97999045 --- /dev/null +++ b/src/common/openai-message-converter.ts @@ -0,0 +1,278 @@ +import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "openai/resources/chat/completions"; +import { supportsMultimodal } from "./model-capabilities"; +import type { SessionMessage } from "../session"; + +export type OpenAIMessageConverterOptions = { + /** Optional callback to render the /init command prompt template. */ + renderInitPrompt?: () => string; +}; + +/** + * Converts internal SessionMessage arrays into OpenAI ChatCompletionMessageParam arrays. + * + * Handles: + * - Tool-call / tool-result pairing with interrupt backfill + * - Thinking-mode reasoning_content injection + * - Multimodal content (images) filtering by model capability + * - Compaction filtering + */ +export class OpenAIMessageConverter { + constructor(private readonly options: OpenAIMessageConverterOptions = {}) {} + + /** + * Build the OpenAI messages array from session messages, applying compaction + * filtering, tool pairing, and format conversion. + */ + buildMessages(messages: SessionMessage[], thinkingEnabled: boolean, model: string): ChatCompletionMessageParam[] { + const activeMessages = messages.filter((message) => !message.compacted); + const toolPairings = this.pairToolMessages(activeMessages); + const openAIMessages: ChatCompletionMessageParam[] = []; + + for (let index = 0; index < activeMessages.length; index += 1) { + const message = activeMessages[index]; + if (message.role === "tool") { + continue; + } + + openAIMessages.push(this.convertMessage(message, thinkingEnabled, model)); + + const toolCalls = this.getAssistantToolCalls(message); + if (toolCalls.length === 0) { + continue; + } + + for (let toolCallIndex = 0; toolCallIndex < toolCalls.length; toolCallIndex += 1) { + const toolCallId = this.getToolCallId(toolCalls[toolCallIndex]); + if (!toolCallId) { + continue; + } + + const pairedToolIndex = toolPairings.get(this.buildToolPairingKey(index, toolCallIndex)); + if (pairedToolIndex != null) { + openAIMessages.push(this.convertMessage(activeMessages[pairedToolIndex], thinkingEnabled, model)); + continue; + } + + openAIMessages.push(this.buildInterruptedOpenAIToolMessage(toolCalls, toolCallId)); + } + } + + return openAIMessages; + } + + /** + * Returns the trailing assistant message with pending (unexecuted) tool calls, + * if one exists at the end of the conversation. + */ + getTrailingPendingToolCallMessage( + messages: SessionMessage[] + ): { message: SessionMessage; toolCalls: unknown[] } | { message: null; toolCalls: [] } { + const activeMessages = messages.filter((message) => !message.compacted); + const latestMessage = activeMessages[activeMessages.length - 1]; + if (!latestMessage || latestMessage.role !== "assistant") { + return { message: null, toolCalls: [] }; + } + + const toolCalls = this.getAssistantToolCalls(latestMessage); + if (toolCalls.length === 0) { + return { message: null, toolCalls: [] }; + } + return { + message: latestMessage, + toolCalls: toolCalls.filter((toolCall) => Boolean(this.getToolCallId(toolCall))), + }; + } + + // --------------------------------------------------------------------------- + // Private helpers + // --------------------------------------------------------------------------- + + private convertMessage(message: SessionMessage, thinkingEnabled: boolean, model: string): ChatCompletionMessageParam { + const content = this.renderContent(message); + const base: ChatCompletionMessageParam = { + role: message.role, + content, + } as ChatCompletionMessageParam; + + const messageParams = message.messageParams as + | { tool_calls?: unknown[]; tool_call_id?: string; reasoning_content?: string } + | null + | undefined; + if (messageParams?.tool_calls) { + (base as { tool_calls?: unknown[] }).tool_calls = messageParams.tool_calls; + } + if (messageParams?.tool_call_id) { + (base as { tool_call_id?: string }).tool_call_id = messageParams.tool_call_id; + } + if (typeof messageParams?.reasoning_content === "string") { + (base as { reasoning_content?: string }).reasoning_content = messageParams.reasoning_content; + } else if (thinkingEnabled && message.role === "assistant") { + // Thinking-mode providers require every replayed assistant message + // to include the reasoning_content field, even when it is empty. + (base as { reasoning_content?: string }).reasoning_content = ""; + } + + if ((message.role === "user" || message.role === "system") && message.contentParams) { + const contentParts: ChatCompletionContentPart[] = []; + if (content) { + contentParts.push({ type: "text", text: content }); + } + const params = Array.isArray(message.contentParams) ? message.contentParams : [message.contentParams]; + for (const param of params) { + const part = param as ChatCompletionContentPart; + if (part && (part.type !== "image_url" || supportsMultimodal(model))) { + contentParts.push(part); + } + } + const contentValue: string | ChatCompletionContentPart[] = contentParts.length > 0 ? contentParts : content; + (base as { content: string | ChatCompletionContentPart[] }).content = contentValue; + } + + return base; + } + + private renderContent(message: SessionMessage): string { + if (message.role === "user" && message.content === "/init") { + return this.options.renderInitPrompt?.() ?? ""; + } + return message.content ?? ""; + } + + private pairToolMessages(messages: SessionMessage[]): Map { + const pairings = new Map(); + const usedToolMessageIndexes = new Set(); + + for (let assistantIndex = 0; assistantIndex < messages.length; assistantIndex += 1) { + const toolCalls = this.getAssistantToolCalls(messages[assistantIndex]); + for (let toolCallIndex = 0; toolCallIndex < toolCalls.length; toolCallIndex += 1) { + const toolCallId = this.getToolCallId(toolCalls[toolCallIndex]); + if (!toolCallId) { + continue; + } + + const toolIndex = this.findPairableToolMessageIndex( + messages, + assistantIndex, + toolCallId, + usedToolMessageIndexes + ); + if (toolIndex == null) { + continue; + } + + usedToolMessageIndexes.add(toolIndex); + pairings.set(this.buildToolPairingKey(assistantIndex, toolCallIndex), toolIndex); + } + } + + return pairings; + } + + private findPairableToolMessageIndex( + messages: SessionMessage[], + assistantIndex: number, + toolCallId: string, + usedToolMessageIndexes: Set + ): number | null { + let firstMatchingIndex: number | null = null; + for (let index = assistantIndex + 1; index < messages.length; index += 1) { + const message = messages[index]; + if (message.role !== "tool" || usedToolMessageIndexes.has(index)) { + continue; + } + + const candidateToolCallId = this.getToolMessageCallId(message); + if (candidateToolCallId !== toolCallId) { + continue; + } + + if (firstMatchingIndex == null) { + firstMatchingIndex = index; + } + if (!this.isInterruptedToolMessage(message)) { + return index; + } + } + return firstMatchingIndex; + } + + private getAssistantToolCalls(message: SessionMessage): unknown[] { + if (message.role !== "assistant") { + return []; + } + const messageParams = message.messageParams as { tool_calls?: unknown[] } | null; + return Array.isArray(messageParams?.tool_calls) ? messageParams.tool_calls : []; + } + + private getToolCallId(toolCall: unknown): string | null { + if (!toolCall || typeof toolCall !== "object") { + return null; + } + const id = (toolCall as { id?: unknown }).id; + return typeof id === "string" && id ? id : null; + } + + private getToolMessageCallId(message: SessionMessage): string | null { + const messageParams = message.messageParams as { tool_call_id?: unknown } | null; + const toolCallId = messageParams?.tool_call_id; + return typeof toolCallId === "string" && toolCallId ? toolCallId : null; + } + + private buildToolPairingKey(assistantIndex: number, toolCallIndex: number): string { + return `${assistantIndex}:${toolCallIndex}`; + } + + private isInterruptedToolMessage(message: SessionMessage): boolean { + if (typeof message.content !== "string" || !message.content.trim()) { + return false; + } + try { + const parsed = JSON.parse(message.content) as { metadata?: { interrupted?: unknown } }; + return parsed.metadata?.interrupted === true; + } catch { + return false; + } + } + + private buildInterruptedOpenAIToolMessage(toolCalls: unknown[], toolCallId: string): ChatCompletionMessageParam { + const toolFunction = this.findToolFunction(toolCalls, toolCallId); + return { + role: "tool", + content: this.buildInterruptedToolResult(toolFunction, "Previous tool call did not complete."), + tool_call_id: toolCallId, + } as ChatCompletionMessageParam; + } + + /** Exposed for use by appendToolMessages in SessionManager. */ + findToolFunction(toolCalls: unknown[], toolCallId: string): unknown | null { + for (const toolCall of toolCalls) { + if (!toolCall || typeof toolCall !== "object") { + continue; + } + const record = toolCall as { id?: unknown; function?: unknown }; + if (record.id === toolCallId) { + return record.function ?? null; + } + } + return null; + } + + private buildInterruptedToolResult(toolFunction: unknown | null, reason: string): string { + const toolName = + toolFunction && typeof toolFunction === "object" && typeof (toolFunction as { name?: unknown }).name === "string" + ? (toolFunction as { name: string }).name + : "tool"; + return JSON.stringify( + { + ok: false, + name: toolName, + error: reason, + metadata: { + interrupted: true, + }, + }, + null, + 2 + ); + } +} diff --git a/src/session.ts b/src/session.ts index 57108dee..0e2b29b9 100644 --- a/src/session.ts +++ b/src/session.ts @@ -4,10 +4,10 @@ import * as os from "os"; import * as crypto from "crypto"; import matter from "gray-matter"; import ejs from "ejs"; -import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "openai/resources/chat/completions"; +import type { ChatCompletionMessageParam } from "openai/resources/chat/completions"; import { launchNotifyScript } from "./common/notify"; import { buildThinkingRequestOptions } from "./common/openai-thinking"; -import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilities"; +import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; import { getCompactPrompt, getDefaultSkillPrompt, @@ -46,6 +46,7 @@ import { } from "./common/permissions"; import { clearSessionWorkingDir } from "./tools/bash-handler"; import { reportNewPrompt } from "./common/telemetry"; +import { OpenAIMessageConverter } from "./common/openai-message-converter"; export type { PermissionScope } from "./settings"; export type { @@ -333,6 +334,7 @@ export class SessionManager { private readonly toolExecutor: ToolExecutor; private readonly mcpManager = new McpManager(); private mcpToolDefinitions: ToolDefinition[] = []; + private readonly messageConverter: OpenAIMessageConverter; constructor(options: SessionManagerOptions) { this.projectRoot = options.projectRoot; @@ -345,6 +347,21 @@ export class SessionManager { this.onProcessStdout = options.onProcessStdout; this.toolExecutor = new ToolExecutor(this.projectRoot, this.createOpenAIClient, this.mcpManager); this.mcpManager.prepare(this.getResolvedSettings().mcpServers); + this.messageConverter = new OpenAIMessageConverter({ + renderInitPrompt: () => this.renderInitCommandPrompt(), + }); + } + + /** + * @deprecated Use messageConverter.buildMessages directly. + * Kept for test compatibility. + */ + buildOpenAIMessages( + messages: SessionMessage[], + thinkingEnabled: boolean, + model: string + ): ChatCompletionMessageParam[] { + return this.messageConverter.buildMessages(messages, thinkingEnabled, model); } async initMcpServers(servers?: Record): Promise { @@ -1234,7 +1251,9 @@ ${skillMd} return; } - const pendingToolCallMessage = this.getTrailingPendingToolCallMessage(this.listSessionMessages(sessionId)); + const pendingToolCallMessage = this.messageConverter.getTrailingPendingToolCallMessage( + this.listSessionMessages(sessionId) + ); if (pendingToolCallMessage.toolCalls.length > 0) { const toolAppendResult = await this.appendToolMessages(sessionId, pendingToolCallMessage.toolCalls, { permissionOverrides: permissionPrompt?.permissions, @@ -1267,7 +1286,11 @@ ${skillMd} await this.compactSession(sessionId, sessionController.signal); } - const messages = this.buildOpenAIMessages(this.listSessionMessages(sessionId), thinkingEnabled, model); + const messages = this.messageConverter.buildMessages( + this.listSessionMessages(sessionId), + thinkingEnabled, + model + ); const thinkingOptions = buildThinkingRequestOptions(thinkingEnabled, baseURL, reasoningEffort); const response = await this.createChatCompletionStream( client, @@ -2201,7 +2224,7 @@ ${skillMd} if (execution.result.awaitUserResponse === true) { waitingForUser = true; } - const toolFunction = this.findToolFunction(toolCalls, execution.toolCallId); + const toolFunction = this.messageConverter.findToolFunction(toolCalls, execution.toolCallId); const toolMessage = this.buildToolMessage(sessionId, execution.toolCallId, execution.content, toolFunction); this.appendSessionMessage(sessionId, toolMessage); this.onAssistantMessage(toolMessage, true); @@ -2233,7 +2256,9 @@ ${skillMd} } private hasTrailingPendingToolCalls(sessionId: string): boolean { - return this.getTrailingPendingToolCallMessage(this.listSessionMessages(sessionId)).toolCalls.length > 0; + return ( + this.messageConverter.getTrailingPendingToolCallMessage(this.listSessionMessages(sessionId)).toolCalls.length > 0 + ); } private async appendDeferredPermissionPrompt( @@ -2288,241 +2313,6 @@ ${skillMd} return undefined; } - private buildOpenAIMessages( - messages: SessionMessage[], - thinkingEnabled: boolean, - model: string - ): ChatCompletionMessageParam[] { - const activeMessages = messages.filter((message) => !message.compacted); - const toolPairings = this.pairToolMessages(activeMessages); - const openAIMessages: ChatCompletionMessageParam[] = []; - - for (let index = 0; index < activeMessages.length; index += 1) { - const message = activeMessages[index]; - if (message.role === "tool") { - continue; - } - - openAIMessages.push(this.sessionMessageToOpenAIMessage(message, thinkingEnabled, model)); - - const toolCalls = this.getAssistantToolCalls(message); - if (toolCalls.length === 0) { - continue; - } - - for (let toolCallIndex = 0; toolCallIndex < toolCalls.length; toolCallIndex += 1) { - const toolCallId = this.getToolCallId(toolCalls[toolCallIndex]); - if (!toolCallId) { - continue; - } - - const pairedToolIndex = toolPairings.get(this.buildToolPairingKey(index, toolCallIndex)); - if (pairedToolIndex != null) { - openAIMessages.push( - this.sessionMessageToOpenAIMessage(activeMessages[pairedToolIndex], thinkingEnabled, model) - ); - continue; - } - - openAIMessages.push(this.buildInterruptedOpenAIToolMessage(toolCalls, toolCallId)); - } - } - - return openAIMessages; - } - - private sessionMessageToOpenAIMessage( - message: SessionMessage, - thinkingEnabled: boolean, - model: string - ): ChatCompletionMessageParam { - const content = this.renderOpenAIMessageContent(message); - const base: ChatCompletionMessageParam = { - role: message.role, - content, - } as ChatCompletionMessageParam; - - const messageParams = message.messageParams as - | { tool_calls?: unknown[]; tool_call_id?: string; reasoning_content?: string } - | null - | undefined; - if (messageParams?.tool_calls) { - (base as { tool_calls?: unknown[] }).tool_calls = messageParams.tool_calls; - } - if (messageParams?.tool_call_id) { - (base as { tool_call_id?: string }).tool_call_id = messageParams.tool_call_id; - } - if (typeof messageParams?.reasoning_content === "string") { - (base as { reasoning_content?: string }).reasoning_content = messageParams.reasoning_content; - } else if (thinkingEnabled && message.role === "assistant") { - // Thinking-mode providers require every replayed assistant message - // to include the reasoning_content field, even when it is empty. - (base as { reasoning_content?: string }).reasoning_content = ""; - } - - if ((message.role === "user" || message.role === "system") && message.contentParams) { - const contentParts: ChatCompletionContentPart[] = []; - if (content) { - contentParts.push({ type: "text", text: content }); - } - const params = Array.isArray(message.contentParams) ? message.contentParams : [message.contentParams]; - for (const param of params) { - const part = param as ChatCompletionContentPart; - if (part && (part.type !== "image_url" || supportsMultimodal(model))) { - contentParts.push(part); - } - } - const contentValue: string | ChatCompletionContentPart[] = contentParts.length > 0 ? contentParts : content; - (base as { content: string | ChatCompletionContentPart[] }).content = contentValue; - } - - return base; - } - - private renderOpenAIMessageContent(message: SessionMessage): string { - if (message.role === "user" && message.content === "/init") { - return this.renderInitCommandPrompt(); - } - return message.content ?? ""; - } - - private pairToolMessages(messages: SessionMessage[]): Map { - const pairings = new Map(); - const usedToolMessageIndexes = new Set(); - - for (let assistantIndex = 0; assistantIndex < messages.length; assistantIndex += 1) { - const toolCalls = this.getAssistantToolCalls(messages[assistantIndex]); - for (let toolCallIndex = 0; toolCallIndex < toolCalls.length; toolCallIndex += 1) { - const toolCallId = this.getToolCallId(toolCalls[toolCallIndex]); - if (!toolCallId) { - continue; - } - - const toolIndex = this.findPairableToolMessageIndex( - messages, - assistantIndex, - toolCallId, - usedToolMessageIndexes - ); - if (toolIndex == null) { - continue; - } - - usedToolMessageIndexes.add(toolIndex); - pairings.set(this.buildToolPairingKey(assistantIndex, toolCallIndex), toolIndex); - } - } - - return pairings; - } - - private getTrailingPendingToolCallMessage( - messages: SessionMessage[] - ): { message: SessionMessage; toolCalls: unknown[] } | { message: null; toolCalls: [] } { - const activeMessages = messages.filter((message) => !message.compacted); - const latestMessage = activeMessages[activeMessages.length - 1]; - if (!latestMessage || latestMessage.role !== "assistant") { - return { message: null, toolCalls: [] }; - } - - const toolCalls = this.getAssistantToolCalls(latestMessage); - if (toolCalls.length === 0) { - return { message: null, toolCalls: [] }; - } - return { - message: latestMessage, - toolCalls: toolCalls.filter((toolCall) => Boolean(this.getToolCallId(toolCall))), - }; - } - - private findPairableToolMessageIndex( - messages: SessionMessage[], - assistantIndex: number, - toolCallId: string, - usedToolMessageIndexes: Set - ): number | null { - let firstMatchingIndex: number | null = null; - for (let index = assistantIndex + 1; index < messages.length; index += 1) { - const message = messages[index]; - if (message.role !== "tool" || usedToolMessageIndexes.has(index)) { - continue; - } - - const candidateToolCallId = this.getToolMessageCallId(message); - if (candidateToolCallId !== toolCallId) { - continue; - } - - if (firstMatchingIndex == null) { - firstMatchingIndex = index; - } - if (!this.isInterruptedToolMessage(message)) { - return index; - } - } - return firstMatchingIndex; - } - - private getAssistantToolCalls(message: SessionMessage): unknown[] { - if (message.role !== "assistant") { - return []; - } - const messageParams = message.messageParams as { tool_calls?: unknown[] } | null; - return Array.isArray(messageParams?.tool_calls) ? messageParams.tool_calls : []; - } - - private getToolCallId(toolCall: unknown): string | null { - if (!toolCall || typeof toolCall !== "object") { - return null; - } - const id = (toolCall as { id?: unknown }).id; - return typeof id === "string" && id ? id : null; - } - - private getToolMessageCallId(message: SessionMessage): string | null { - const messageParams = message.messageParams as { tool_call_id?: unknown } | null; - const toolCallId = messageParams?.tool_call_id; - return typeof toolCallId === "string" && toolCallId ? toolCallId : null; - } - - private buildToolPairingKey(assistantIndex: number, toolCallIndex: number): string { - return `${assistantIndex}:${toolCallIndex}`; - } - - private isInterruptedToolMessage(message: SessionMessage): boolean { - if (typeof message.content !== "string" || !message.content.trim()) { - return false; - } - try { - const parsed = JSON.parse(message.content) as { metadata?: { interrupted?: unknown } }; - return parsed.metadata?.interrupted === true; - } catch { - return false; - } - } - - private buildInterruptedOpenAIToolMessage(toolCalls: unknown[], toolCallId: string): ChatCompletionMessageParam { - const toolFunction = this.findToolFunction(toolCalls, toolCallId); - return { - role: "tool", - content: this.buildInterruptedToolResult(toolFunction, "Previous tool call did not complete."), - tool_call_id: toolCallId, - } as ChatCompletionMessageParam; - } - - private findToolFunction(toolCalls: unknown[], toolCallId: string): unknown | null { - for (const toolCall of toolCalls) { - if (!toolCall || typeof toolCall !== "object") { - continue; - } - const record = toolCall as { id?: unknown; function?: unknown }; - if (record.id === toolCallId) { - return record.function ?? null; - } - } - return null; - } - private buildToolParamsSnippet(toolFunction: unknown | null): string { if (!toolFunction || typeof toolFunction !== "object") { return ""; @@ -2756,25 +2546,6 @@ ${skillMd} return ids; } - private buildInterruptedToolResult(toolFunction: unknown | null, reason: string): string { - const toolName = - toolFunction && typeof toolFunction === "object" && typeof (toolFunction as { name?: unknown }).name === "string" - ? (toolFunction as { name: string }).name - : "tool"; - return JSON.stringify( - { - ok: false, - name: toolName, - error: reason, - metadata: { - interrupted: true, - }, - }, - null, - 2 - ); - } - private normalizeSessionEntry(entry: unknown): SessionEntry { const value = entry && typeof entry === "object" ? (entry as Record) : {}; return { From cd433cd696c0f2aea07960dccc2c2dbad8e12190 Mon Sep 17 00:00:00 2001 From: lellansin Date: Sat, 30 May 2026 17:06:37 +0800 Subject: [PATCH 122/212] test: add unit tests for OpenAIMessageConverter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 20 tests covering buildMessages (content handling, tool-call pairing, interrupted backfill, compaction filtering), getTrailingPendingToolCallMessage, and findToolFunction. No dependency on SessionManager — pure data-in / data-out. --- src/tests/openai-message-converter.test.ts | 508 +++++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 src/tests/openai-message-converter.test.ts diff --git a/src/tests/openai-message-converter.test.ts b/src/tests/openai-message-converter.test.ts new file mode 100644 index 00000000..a54c213d --- /dev/null +++ b/src/tests/openai-message-converter.test.ts @@ -0,0 +1,508 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { OpenAIMessageConverter } from "../common/openai-message-converter"; +import type { SessionMessage } from "../session"; + +// --------------------------------------------------------------------------- +// Test helpers — build SessionMessage objects without needing SessionManager +// --------------------------------------------------------------------------- + +function msg(overrides: Partial & { role: SessionMessage["role"] }): SessionMessage { + const now = "2026-01-01T00:00:00.000Z"; + return { + id: overrides.id ?? "msg-1", + sessionId: overrides.sessionId ?? "session-1", + role: overrides.role, + content: overrides.content ?? null, + contentParams: overrides.contentParams ?? null, + messageParams: overrides.messageParams ?? null, + compacted: overrides.compacted ?? false, + visible: overrides.visible ?? true, + createTime: overrides.createTime ?? now, + updateTime: overrides.updateTime ?? now, + meta: overrides.meta, + }; +} + +function assistantMsg( + id: string, + toolCalls?: Array<{ id: string; type?: string; function: { name: string; arguments: string } }>, + reasoningContent?: string | null +): SessionMessage { + const hasTcs = toolCalls && toolCalls.length > 0; + const hasReasoning = reasoningContent !== undefined && reasoningContent !== null; + const messageParams: Record | null = hasTcs || hasReasoning ? {} : null; + if (hasTcs) (messageParams as Record).tool_calls = toolCalls; + if (hasReasoning) (messageParams as Record).reasoning_content = reasoningContent; + return msg({ + id, + role: "assistant", + content: "", + messageParams, + visible: false, + }); +} + +function toolMsg( + id: string, + toolCallId: string, + content: string, + toolFunction?: { name: string; arguments: string } +): SessionMessage { + return msg({ + id, + role: "tool", + content, + messageParams: { tool_call_id: toolCallId }, + meta: toolFunction ? { function: toolFunction } : undefined, + }); +} + +function userMsg(id: string, content: string): SessionMessage { + return msg({ id, role: "user", content }); +} + +// --------------------------------------------------------------------------- +// Converter fixtures +// --------------------------------------------------------------------------- + +function converter(opts?: { renderInitPrompt?: () => string }) { + return new OpenAIMessageConverter(opts); +} + +// --------------------------------------------------------------------------- +// buildMessages — content handling +// --------------------------------------------------------------------------- + +test("OpenAIMessageConverter preserves image content for multimodal models", () => { + const c = converter(); + const messages: SessionMessage[] = [ + msg({ + role: "system", + content: "Loaded pixel.png", + contentParams: [{ type: "image_url", image_url: { url: "data:image/png;base64,abc" } }], + }), + ]; + + const result = c.buildMessages(messages, false, "gpt-4o") as Array<{ role: string; content: unknown }>; + + assert.equal(result.length, 1); + assert.equal(result[0]?.role, "system"); + assert.deepEqual(result[0]?.content, [ + { type: "text", text: "Loaded pixel.png" }, + { type: "image_url", image_url: { url: "data:image/png;base64,abc" } }, + ]); +}); + +test("OpenAIMessageConverter filters image content for non-multimodal models", () => { + const c = converter(); + const messages: SessionMessage[] = [ + msg({ + role: "system", + content: "Loaded pixel.png", + contentParams: [{ type: "image_url", image_url: { url: "data:image/png;base64,abc" } }], + }), + ]; + + const result = c.buildMessages(messages, false, "deepseek-chat") as Array<{ role: string; content: unknown }>; + + assert.equal(result.length, 1); + assert.deepEqual(result[0]?.content, [{ type: "text", text: "Loaded pixel.png" }]); +}); + +test("OpenAIMessageConverter injects reasoning_content in thinking mode", () => { + const c = converter(); + const messages: SessionMessage[] = [msg({ role: "assistant", content: "Final answer", messageParams: null })]; + + const thinking = c.buildMessages(messages, true, "test-model") as Array<{ reasoning_content?: string }>; + const nonThinking = c.buildMessages(messages, false, "test-model") as Array<{ reasoning_content?: string }>; + + assert.equal(thinking[0]?.reasoning_content, ""); + assert.equal(Object.prototype.hasOwnProperty.call(nonThinking[0] ?? {}, "reasoning_content"), false); +}); + +test("OpenAIMessageConverter preserves existing reasoning_content from messageParams", () => { + const c = converter(); + const messages: SessionMessage[] = [ + msg({ + role: "assistant", + content: "answer", + messageParams: { reasoning_content: "deep thought" }, + }), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ reasoning_content?: string }>; + + assert.equal(result[0]?.reasoning_content, "deep thought"); +}); + +test("OpenAIMessageConverter uses /init prompt via renderInitPrompt callback", () => { + const c = converter({ renderInitPrompt: () => "EXPANDED INIT PROMPT" }); + const messages: SessionMessage[] = [msg({ role: "user", content: "/init" })]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ content: string }>; + + assert.equal(result[0]?.content, "EXPANDED INIT PROMPT"); +}); + +test("OpenAIMessageConverter skips compacted messages", () => { + const c = converter(); + const messages: SessionMessage[] = [ + userMsg("u1", "hello"), + msg({ id: "a1", role: "assistant", content: "hi", compacted: true }), + userMsg("u2", "still here?"), + msg({ id: "a2", role: "assistant", content: "yes" }), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ role: string }>; + + assert.deepEqual( + result.map((m) => m.role), + ["user", "user", "assistant"] + ); +}); + +// --------------------------------------------------------------------------- +// buildMessages — tool-call pairing +// --------------------------------------------------------------------------- + +test("OpenAIMessageConverter preserves a complete multi-tool happy path", () => { + const c = converter(); + const messages: SessionMessage[] = [ + assistantMsg("a1", [ + { id: "call-1", type: "function", function: { name: "read", arguments: '{"file_path":"/tmp/a.txt"}' } }, + { id: "call-2", type: "function", function: { name: "bash", arguments: '{"command":"pwd"}' } }, + ]), + toolMsg("t1", "call-1", JSON.stringify({ ok: true, name: "read", content: "file content" }), { + name: "read", + arguments: '{"file_path":"/tmp/a.txt"}', + }), + toolMsg("t2", "call-2", JSON.stringify({ ok: true, name: "bash", output: "/tmp\n" }), { + name: "bash", + arguments: '{"command":"pwd"}', + }), + userMsg("u1", "thanks"), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ + role: string; + tool_call_id?: string; + content: string; + }>; + + assert.deepEqual( + result.map((m) => m.role), + ["assistant", "tool", "tool", "user"] + ); + assert.deepEqual( + result.filter((m) => m.role === "tool").map((m) => m.tool_call_id), + ["call-1", "call-2"] + ); + const hasInterrupted = result.some((m) => m.content.includes("Previous tool call did not complete")); + assert.equal(hasInterrupted, false); +}); + +test("OpenAIMessageConverter inserts interrupted backfill for missing tool messages", () => { + const c = converter(); + const messages: SessionMessage[] = [ + assistantMsg("a1", [ + { id: "call-1", type: "function", function: { name: "bash", arguments: '{"command":"sleep 100"}' } }, + ]), + userMsg("u1", "continue"), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ + role: string; + content: string; + tool_call_id?: string; + }>; + + assert.equal(result.length, 3); + assert.equal(result[0]?.role, "assistant"); + assert.equal(result[1]?.role, "tool"); + assert.equal(result[1]?.tool_call_id, "call-1"); + assert.match(result[1]?.content ?? "", /Previous tool call did not complete/); + assert.equal(result[2]?.role, "user"); +}); + +test("OpenAIMessageConverter ignores orphan tool messages", () => { + const c = converter(); + const messages: SessionMessage[] = [ + userMsg("u1", "hello"), + toolMsg("t1", "call-orphan", JSON.stringify({ ok: true, name: "bash", output: "orphan" }), { + name: "bash", + arguments: '{"command":"echo orphan"}', + }), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ role: string }>; + + assert.deepEqual( + result.map((m) => m.role), + ["user"] + ); +}); + +test("OpenAIMessageConverter prefers first non-interrupted tool result for a tool call", () => { + const c = converter(); + const messages: SessionMessage[] = [ + assistantMsg("a1", [ + { id: "call-1", type: "function", function: { name: "bash", arguments: '{"command":"date"}' } }, + ]), + toolMsg("t1", "call-1", JSON.stringify({ ok: true, name: "bash", output: "2026-05-07\n" }), { + name: "bash", + arguments: '{"command":"date"}', + }), + toolMsg( + "t2", + "call-1", + JSON.stringify({ + ok: false, + name: "bash", + error: "Previous tool call did not complete.", + metadata: { interrupted: true }, + }), + { name: "bash", arguments: '{"command":"date"}' } + ), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ + role: string; + content: string; + tool_call_id?: string; + }>; + const toolResults = result.filter((m) => m.role === "tool"); + + assert.equal(toolResults.length, 1); + assert.equal(toolResults[0]?.tool_call_id, "call-1"); + assert.match(toolResults[0]?.content ?? "", /2026-05-07/); + assert.doesNotMatch(toolResults[0]?.content ?? "", /Previous tool call did not complete/); +}); + +test("OpenAIMessageConverter prefers later real result over earlier interrupted placeholder", () => { + const c = converter(); + const messages: SessionMessage[] = [ + assistantMsg("a1", [ + { id: "call-1", type: "function", function: { name: "bash", arguments: '{"command":"date"}' } }, + ]), + toolMsg( + "t1", + "call-1", + JSON.stringify({ + ok: false, + name: "bash", + error: "Previous tool call did not complete.", + metadata: { interrupted: true }, + }), + { name: "bash", arguments: '{"command":"date"}' } + ), + toolMsg("t2", "call-1", JSON.stringify({ ok: true, name: "bash", output: "real result" }), { + name: "bash", + arguments: '{"command":"date"}', + }), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ + role: string; + content: string; + tool_call_id?: string; + }>; + const toolResults = result.filter((m) => m.role === "tool"); + + assert.equal(toolResults.length, 1); + assert.match(toolResults[0]?.content ?? "", /real result/); +}); + +test("OpenAIMessageConverter preserves a real failed tool result", () => { + const c = converter(); + const messages: SessionMessage[] = [ + assistantMsg("a1", [ + { id: "call-1", type: "function", function: { name: "bash", arguments: '{"command":"false"}' } }, + ]), + toolMsg( + "t1", + "call-1", + JSON.stringify({ ok: false, name: "bash", error: "Command failed", metadata: { exitCode: 1 } }), + { name: "bash", arguments: '{"command":"false"}' } + ), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ + role: string; + content: string; + tool_call_id?: string; + }>; + + assert.deepEqual( + result.map((m) => m.role), + ["assistant", "tool"] + ); + assert.match(result[1]?.content ?? "", /Command failed/); + assert.doesNotMatch(result[1]?.content ?? "", /Previous tool call did not complete/); +}); + +test("OpenAIMessageConverter repairs mixed missing/duplicate/orphan tool messages", () => { + const c = converter(); + const messages: SessionMessage[] = [ + assistantMsg("a1", [ + { id: "call-1", type: "function", function: { name: "read", arguments: '{"file_path":"/tmp/missing.txt"}' } }, + { id: "call-2", type: "function", function: { name: "bash", arguments: '{"command":"pwd"}' } }, + ]), + toolMsg("t-orphan", "call-orphan", JSON.stringify({ ok: true, name: "bash", output: "orphan" }), { + name: "bash", + arguments: '{"command":"echo orphan"}', + }), + toolMsg("t1", "call-2", JSON.stringify({ ok: true, name: "bash", output: "/tmp\n" }), { + name: "bash", + arguments: '{"command":"pwd"}', + }), + toolMsg("t2", "call-2", JSON.stringify({ ok: true, name: "bash", output: "duplicate" }), { + name: "bash", + arguments: '{"command":"pwd"}', + }), + userMsg("u1", "continue"), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ + role: string; + content: string; + tool_call_id?: string; + }>; + const toolResults = result.filter((m) => m.role === "tool"); + + assert.deepEqual( + result.map((m) => m.role), + ["assistant", "tool", "tool", "user"] + ); + assert.deepEqual( + toolResults.map((m) => m.tool_call_id), + ["call-1", "call-2"] + ); + assert.match(toolResults[0]?.content ?? "", /Previous tool call did not complete/); + assert.match(toolResults[1]?.content ?? "", /\/tmp/); + assert.equal( + result.some((m) => m.content.includes("orphan")), + false + ); + assert.equal( + result.some((m) => m.content.includes("duplicate")), + false + ); +}); + +test("OpenAIMessageConverter ignores tool messages before their assistant", () => { + const c = converter(); + const messages: SessionMessage[] = [ + toolMsg("t1", "call-1", JSON.stringify({ ok: true, name: "bash", output: "too early" }), { + name: "bash", + arguments: '{"command":"date"}', + }), + assistantMsg("a1", [ + { id: "call-1", type: "function", function: { name: "bash", arguments: '{"command":"date"}' } }, + ]), + ]; + + const result = c.buildMessages(messages, false, "test-model") as Array<{ + role: string; + content: string; + tool_call_id?: string; + }>; + + assert.deepEqual( + result.map((m) => m.role), + ["assistant", "tool"] + ); + assert.match(result[1]?.content ?? "", /Previous tool call did not complete/); + assert.doesNotMatch(result[1]?.content ?? "", /too early/); +}); + +// --------------------------------------------------------------------------- +// getTrailingPendingToolCallMessage +// --------------------------------------------------------------------------- + +test("OpenAIMessageConverter.getTrailingPendingToolCallMessage finds pending tools", () => { + const c = converter(); + const messages: SessionMessage[] = [ + userMsg("u1", "hello"), + assistantMsg("a1", [ + { id: "call-1", type: "function", function: { name: "bash", arguments: '{"command":"date"}' } }, + ]), + ]; + + const result = c.getTrailingPendingToolCallMessage(messages); + + assert.notEqual(result.message, null); + assert.deepEqual( + result.toolCalls.map((tc) => (tc as { id: string }).id), + ["call-1"] + ); +}); + +test("OpenAIMessageConverter.getTrailingPendingToolCallMessage returns empty when latest is user", () => { + const c = converter(); + const messages: SessionMessage[] = [userMsg("u1", "hello")]; + + const result = c.getTrailingPendingToolCallMessage(messages); + + assert.equal(result.message, null); + assert.deepEqual(result.toolCalls, []); +}); + +test("OpenAIMessageConverter.getTrailingPendingToolCallMessage returns empty when no tool calls", () => { + const c = converter(); + const messages: SessionMessage[] = [msg({ id: "a1", role: "assistant", content: "done" })]; + + const result = c.getTrailingPendingToolCallMessage(messages); + + assert.equal(result.message, null); + assert.deepEqual(result.toolCalls, []); +}); + +test("OpenAIMessageConverter.getTrailingPendingToolCallMessage skips compacted messages", () => { + const c = converter(); + const messages: SessionMessage[] = [ + userMsg("u1", "hello"), + msg({ + id: "a1", + role: "assistant", + content: "", + messageParams: { + tool_calls: [{ id: "call-1", type: "function", function: { name: "bash", arguments: "{}" } }], + }, + compacted: true, + }), + msg({ id: "a2", role: "assistant", content: "done" }), + ]; + + const result = c.getTrailingPendingToolCallMessage(messages); + + assert.equal(result.message, null); + assert.deepEqual(result.toolCalls, []); +}); + +// --------------------------------------------------------------------------- +// findToolFunction +// --------------------------------------------------------------------------- + +test("OpenAIMessageConverter.findToolFunction finds matching tool function", () => { + const c = converter(); + const toolCalls = [ + { id: "call-1", type: "function", function: { name: "read", arguments: '{"file_path":"/tmp/a.txt"}' } }, + { id: "call-2", type: "function", function: { name: "bash", arguments: '{"command":"pwd"}' } }, + ]; + + const found = c.findToolFunction(toolCalls, "call-1") as { name: string }; + assert.equal(found?.name, "read"); + + const notFound = c.findToolFunction(toolCalls, "call-3"); + assert.equal(notFound, null); +}); + +test("OpenAIMessageConverter.findToolFunction handles null/empty toolCalls", () => { + const c = converter(); + + assert.equal(c.findToolFunction([], "call-1"), null); + + const toolCalls = [null, undefined, { noId: true }]; + assert.equal(c.findToolFunction(toolCalls as unknown[], "call-1"), null); +}); From 63c7884f8f8371e7f9c11b9b9006993c89137778 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 2 Jun 2026 13:33:12 +0800 Subject: [PATCH 123/212] feat: enhance Bash tool for background task cleanup logic --- src/session.ts | 95 ++++++++++++++++++++++++----- src/tests/prompt.test.ts | 4 +- src/tests/session.test.ts | 104 ++++++++++++++++++++++++++++++++ src/tests/tool-handlers.test.ts | 39 ++++++++++++ src/tools/bash-handler.ts | 30 ++++++++- templates/tools/bash.md | 4 +- 6 files changed, 256 insertions(+), 20 deletions(-) diff --git a/src/session.ts b/src/session.ts index 1f3cca7b..6d1329ab 100644 --- a/src/session.ts +++ b/src/session.ts @@ -8,6 +8,7 @@ import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "open import { launchNotifyScript } from "./common/notify"; import { buildThinkingRequestOptions } from "./common/openai-thinking"; import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilities"; +import { readTextFileWithMetadata } from "./common/file-utils"; import { getCompactPrompt, getDefaultSkillPrompt, @@ -60,6 +61,7 @@ export type { const MAX_SESSION_ENTRIES = 50; const MAX_PROJECT_CODE_LENGTH = 64; const PROJECT_CODE_HASH_LENGTH = 16; +const BACKGROUND_FAILURE_LOG_TAIL_CHARS = 4000; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; @@ -330,6 +332,7 @@ export class SessionManager { private activePromptController: AbortController | null = null; private readonly sessionControllers = new Map(); private readonly processTimeoutControls = new Map(); + private readonly liveProcessKeys = new Set(); private readonly toolExecutor: ToolExecutor; private readonly mcpManager = new McpManager(); private mcpToolDefinitions: ToolDefinition[] = []; @@ -379,6 +382,7 @@ export class SessionManager { sessionController.abort(); } } + this.killLiveProcesses(); this.sessionControllers.clear(); this.processTimeoutControls.clear(); this.mcpManager.disconnect(); @@ -1525,7 +1529,9 @@ ${skillMd} const killedPids: number[] = []; const failedPids: number[] = []; for (const pid of processIds) { - this.processTimeoutControls.delete(this.getProcessControlKey(sessionId, pid)); + const processControlKey = this.getProcessControlKey(sessionId, pid); + this.processTimeoutControls.delete(processControlKey); + this.liveProcessKeys.delete(processControlKey); if (killProcessTree(pid, "SIGKILL")) { killedPids.push(pid); continue; @@ -1892,21 +1898,11 @@ ${skillMd} const processIds = options.processIds ?? []; for (const pid of processIds) { const processControlKey = this.getProcessControlKey(sessionId, pid); - if (!this.processTimeoutControls.has(processControlKey)) { + if (!this.processTimeoutControls.has(processControlKey) && !this.liveProcessKeys.has(processControlKey)) { continue; } - const killedGroup = killProcessTree(pid, "SIGKILL"); - if (killedGroup) { - this.processTimeoutControls.delete(processControlKey); - continue; - } - try { - process.kill(pid, "SIGKILL"); - } catch { - // ignore process-kill failures during cleanup - } - this.processTimeoutControls.delete(processControlKey); + this.killTrackedProcess(processControlKey, pid); } clearSessionState(sessionId); @@ -2667,6 +2663,7 @@ ${skillMd} private addSessionProcess(sessionId: string, processId: string | number, command: string): void { const now = new Date().toISOString(); + this.liveProcessKeys.add(this.getProcessControlKey(sessionId, processId)); this.updateSessionEntry(sessionId, (entry) => { const processes = new Map(entry.processes ?? []); processes.set(String(processId), { startTime: now, command }); @@ -2699,12 +2696,47 @@ ${skillMd} ? `signal ${completion.signal}` : completion.error || "unknown status"; const durationMs = Math.max(0, completion.completedAtMs - completion.startedAtMs); - const content = + const baseContent = `Background command "${completion.command}" ${status} with ${exitText} ` + `after ${this.formatBackgroundDuration(durationMs)}. Output: ${completion.outputPath}`; + const logTail = completion.ok ? null : this.buildBackgroundFailureLogTailSlice(completion.outputPath); + const content = logTail ? `${baseContent}\n${logTail}` : baseContent; this.addSessionSystemMessage(sessionId, content, true); } + private buildBackgroundFailureLogTailSlice(outputPath: string): string | null { + const tail = this.readTextFileTail(outputPath, BACKGROUND_FAILURE_LOG_TAIL_CHARS); + if (!tail || !tail.content) { + return null; + } + const prefix = tail.truncated ? `(${tail.totalBytes} bytes)...\n` : ""; + return [ + ``, + `${prefix}${tail.content}`, + "", + ].join("\n"); + } + + private readTextFileTail( + filePath: string, + maxChars: number + ): { content: string; totalBytes: number; truncated: boolean } | null { + try { + const stat = fs.statSync(filePath); + if (!stat.isFile() || stat.size <= 0) { + return null; + } + const content = readTextFileWithMetadata(filePath).content; + return { + content: content.slice(-maxChars).trimEnd(), + totalBytes: stat.size, + truncated: content.length > maxChars, + }; + } catch { + return null; + } + } + private formatBackgroundDuration(durationMs: number): string { if (durationMs < 1000) { return `${durationMs}ms`; @@ -2720,7 +2752,9 @@ ${skillMd} private removeSessionProcess(sessionId: string, processId: string | number): void { const now = new Date().toISOString(); - this.processTimeoutControls.delete(this.getProcessControlKey(sessionId, processId)); + const processControlKey = this.getProcessControlKey(sessionId, processId); + this.processTimeoutControls.delete(processControlKey); + this.liveProcessKeys.delete(processControlKey); this.updateSessionEntry(sessionId, (entry) => { const processes = new Map(entry.processes ?? []); processes.delete(String(processId)); @@ -2783,6 +2817,37 @@ ${skillMd} return `${sessionId}:${String(processId)}`; } + private killLiveProcesses(): void { + for (const processControlKey of Array.from(this.liveProcessKeys)) { + const processId = this.getProcessIdFromControlKey(processControlKey); + if (processId === null) { + this.liveProcessKeys.delete(processControlKey); + continue; + } + this.killTrackedProcess(processControlKey, processId); + } + } + + private killTrackedProcess(processControlKey: string, processId: number): void { + const killedGroup = killProcessTree(processId, "SIGKILL"); + if (!killedGroup) { + try { + process.kill(processId, "SIGKILL"); + } catch { + // Ignore process-kill failures during cleanup. + } + } + this.processTimeoutControls.delete(processControlKey); + this.liveProcessKeys.delete(processControlKey); + } + + private getProcessIdFromControlKey(processControlKey: string): number | null { + const separatorIndex = processControlKey.lastIndexOf(":"); + const rawProcessId = separatorIndex >= 0 ? processControlKey.slice(separatorIndex + 1) : processControlKey; + const processId = Number(rawProcessId); + return Number.isInteger(processId) && processId > 0 ? processId : null; + } + private getProcessIds(processes: Map | null): number[] { if (!processes) { return []; diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts index 52024a32..31f16a63 100644 --- a/src/tests/prompt.test.ts +++ b/src/tests/prompt.test.ts @@ -48,7 +48,9 @@ test("getSystemPrompt includes UpdatePlan docs", () => { test("getSystemPrompt includes Bash background guidance", () => { const prompt = getSystemPrompt("/tmp/project"); assert.equal(prompt.includes("run_in_background: true"), true); - assert.equal(prompt.includes("do not add `&`"), true); + assert.equal(prompt.includes("do NOT add `&`"), true); + assert.equal(prompt.includes("use the `stopCommand` returned in the tool result metadata"), true); + assert.equal(prompt.includes("stop background tasks that has not reported a completed state"), true); }); test("getSystemPrompt does not include runtime context", () => { diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 06808ed2..b02642bb 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -108,6 +108,49 @@ test("SessionManager preserves structured system content when building OpenAI me ]); }); +test("SessionManager appends failed background log tail as XML", () => { + const workspace = createTempDir("deepcode-background-log-workspace-"); + const home = createTempDir("deepcode-background-log-home-"); + setHomeDir(home); + const outputPath = path.join(workspace, "background.log"); + fs.writeFileSync(outputPath, ["before", "failure & one", "failure line two"].join("\n"), "utf8"); + let systemMessage: SessionMessage | null = null; + const manager = new SessionManager({ + projectRoot: workspace, + createOpenAIClient: () => ({ + client: null, + model: "test-model", + thinkingEnabled: false, + }), + getResolvedSettings: () => ({ model: "test-model" }), + renderMarkdown: (text) => text, + onAssistantMessage: (message) => { + systemMessage = message; + }, + }); + + (manager as any).addBackgroundProcessCompletionMessage("session-background-fail", { + command: "npm test", + outputPath, + ok: false, + exitCode: 1, + signal: null, + startedAtMs: 0, + completedAtMs: 1200, + }); + + assert.ok(systemMessage); + const message = systemMessage as SessionMessage; + assert.equal(message.role, "system"); + const content = message.content ?? ""; + assert.match(content, /Background command "npm test" failed with exit code 1/); + assert.match(content, new RegExp(``)); + assert.match(content, /failure & one[\s\S]*failure line two/); + assert.doesNotMatch(content, /failure <line> & one/); + assert.doesNotMatch(content, //); + assert.doesNotMatch(content, //); +}); + test("SessionManager filters image content for non-multimodal models", () => { const manager = new SessionManager({ projectRoot: process.cwd(), @@ -499,6 +542,67 @@ rl.on("line", (line) => { assert.deepEqual(manager.getMcpStatus(), []); }); +test("SessionManager dispose kills live processes without timeout controls", (t) => { + if (process.platform === "win32") { + t.skip("process group kill assertion is non-Windows specific"); + return; + } + + const workspace = createTempDir("deepcode-dispose-process-workspace-"); + const home = createTempDir("deepcode-dispose-process-home-"); + setHomeDir(home); + const manager = createSessionManager(workspace, "machine-id-dispose-process"); + const sessionId = createSessionAndMessages(manager, "session-dispose-process", "Dispose process session"); + const originalKill = process.kill; + const killed: Array<{ pid: number; signal?: NodeJS.Signals | number }> = []; + + try { + process.kill = ((pid: number, signal?: NodeJS.Signals | number) => { + killed.push({ pid, signal }); + return true; + }) as typeof process.kill; + + (manager as any).addSessionProcess(sessionId, 1234, "python3 -m http.server 8080"); + manager.dispose(); + } finally { + process.kill = originalKill; + } + + assert.deepEqual(killed, [{ pid: -1234, signal: "SIGKILL" }]); +}); + +test("SessionManager deleteSession ignores persisted processes that are not live", (t) => { + if (process.platform === "win32") { + t.skip("process group kill assertion is non-Windows specific"); + return; + } + + const workspace = createTempDir("deepcode-delete-stale-process-workspace-"); + const home = createTempDir("deepcode-delete-stale-process-home-"); + setHomeDir(home); + const manager = createSessionManager(workspace, "machine-id-delete-stale-process"); + const sessionId = createSessionAndMessages(manager, "session-delete-stale-process", "Delete stale process session"); + (manager as any).updateSessionEntry(sessionId, (entry: any) => ({ + ...entry, + processes: new Map([["1234", { startTime: new Date().toISOString(), command: "stale process" }]]), + })); + const originalKill = process.kill; + const killed: Array<{ pid: number; signal?: NodeJS.Signals | number }> = []; + + try { + process.kill = ((pid: number, signal?: NodeJS.Signals | number) => { + killed.push({ pid, signal }); + return true; + }) as typeof process.kill; + + assert.equal(manager.deleteSession(sessionId), true); + } finally { + process.kill = originalKill; + } + + assert.deepEqual(killed, []); +}); + test("SessionManager refreshes cached MCP tool definitions after server crash", async () => { const workspace = createTempDir("deepcode-mcp-crash-cache-workspace-"); const serverPath = path.join(workspace, "mcp-server-crash.cjs"); diff --git a/src/tests/tool-handlers.test.ts b/src/tests/tool-handlers.test.ts index d070d3e0..735c0274 100644 --- a/src/tests/tool-handlers.test.ts +++ b/src/tests/tool-handlers.test.ts @@ -131,6 +131,13 @@ test("Bash can run commands in the background and report completion output", asy assert.equal(result.metadata?.runInBackground, true); assert.equal(typeof result.metadata?.backgroundTaskId, "string"); assert.equal(typeof result.metadata?.outputPath, "string"); + assert.equal(typeof result.metadata?.processId, "number"); + const stopCommand = + process.platform === "win32" + ? `cmd.exe /c "taskkill /PID ${result.metadata.processId} /T /F"` + : `kill -- -${result.metadata.processId}`; + assert.equal(result.metadata?.stopCommand, stopCommand); + assert.match(result.output ?? "", /Stop it with:/); assert.ok(Date.now() - startedAt < 500); assert.equal(starts.length, 1); @@ -176,6 +183,38 @@ test("Bash background completion reports failed exit codes", async () => { assert.match(output, /bad/); }); +test("Bash removes a trailing ampersand when run_in_background is true", async () => { + const workspace = createTempWorkspace(); + let startedCommand = ""; + let completion: BackgroundProcessCompletion | null = null; + + const result = await handleBashTool( + { + command: "printf 'trimmed\\n' &", + run_in_background: true, + }, + createContext("bash-background-trailing-ampersand", workspace, { + onProcessStart: (_pid, command) => { + startedCommand = command; + }, + onBackgroundProcessComplete: (event) => { + completion = event; + }, + }) + ); + + assert.equal(result.ok, true); + assert.equal(startedCommand, "printf 'trimmed\\n'"); + + await waitFor(() => completion !== null, 2000); + + assert.ok(completion); + const done = completion as BackgroundProcessCompletion; + assert.equal(done.command, "printf 'trimmed\\n'"); + assert.equal(done.ok, true); + assert.equal(fs.readFileSync(done.outputPath, "utf8"), "trimmed\n"); +}); + test("UpdatePlan accepts a markdown task list string", async () => { const workspace = createTempWorkspace(); const plan = ["## Task List", "", "- [>] Inspect current behavior", "- [ ] Implement UpdatePlan"].join("\n"); diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts index ae42a354..5da07944 100644 --- a/src/tools/bash-handler.ts +++ b/src/tools/bash-handler.ts @@ -18,6 +18,7 @@ import { const MAX_OUTPUT_CHARS = 30000; const MAX_CAPTURE_CHARS = 10 * 1024 * 1024; const BACKGROUND_OUTPUT_DIR = path.join(os.tmpdir(), "deepcode-background"); +const TRAILING_BACKGROUND_OPERATOR_PATTERN = /(^|[^\\&])\s*&\s*$/; const sessionWorkingDirs = new Map(); export function clearSessionWorkingDir(sessionId: string): void { @@ -45,7 +46,9 @@ export async function handleBashTool( args: Record, context: ToolExecutionContext ): Promise { - const command = typeof args.command === "string" ? args.command : ""; + const rawCommand = typeof args.command === "string" ? args.command : ""; + const runInBackground = isTrue(args.run_in_background); + const command = runInBackground ? stripTrailingBackgroundOperator(rawCommand) : rawCommand; if (!command.trim()) { return { ok: false, @@ -56,7 +59,6 @@ export async function handleBashTool( const startCwd = getSessionCwd(context.sessionId, context.projectRoot); const { shellPath, shellArgs, marker } = buildShellCommand(command); - const runInBackground = isTrue(args.run_in_background); if (runInBackground) { return startBackgroundShellCommand(shellPath, shellArgs, startCwd, command, marker, context); @@ -89,6 +91,10 @@ function isTrue(value: unknown): boolean { return value === true || value === "true"; } +function stripTrailingBackgroundOperator(command: string): string { + return command.replace(TRAILING_BACKGROUND_OPERATOR_PATTERN, "$1").trimEnd(); +} + function getSessionCwd(sessionId: string, fallback: string): string { return sessionWorkingDirs.get(sessionId) ?? fallback; } @@ -272,6 +278,7 @@ function startBackgroundShellCommand( }); const pid = child.pid; const processId = typeof pid === "number" ? pid : -1; + const stopCommand = typeof pid === "number" ? buildStopBackgroundProcessCommand(pid) : null; let stdout = ""; let stderr = ""; @@ -347,11 +354,12 @@ function startBackgroundShellCommand( return { ok: true, name: "bash", - output: `Command running in background with ID: ${taskId}. Output is being written to: ${outputPath}`, + output: buildBackgroundStartMessage(taskId, outputPath, stopCommand), metadata: { backgroundTaskId: taskId, processId: typeof pid === "number" ? pid : null, outputPath, + stopCommand, cwd, shellPath, startCwd: cwd, @@ -360,6 +368,22 @@ function startBackgroundShellCommand( }; } +function buildBackgroundStartMessage(taskId: string, outputPath: string, stopCommand: string | null): string { + const parts = [`Command running in background with ID: ${taskId}.`]; + if (stopCommand) { + parts.push(`Stop it with: ${stopCommand}`); + } + parts.push(`Output is being written to: ${outputPath}`); + return parts.join(" "); +} + +function buildStopBackgroundProcessCommand(processId: number): string { + if (process.platform === "win32") { + return `cmd.exe /c "taskkill /PID ${processId} /T /F"`; + } + return `kill -- -${processId}`; +} + function writeFinalBackgroundOutput(outputPath: string, output: string | undefined): void { try { fs.writeFileSync(outputPath, output ?? "", "utf8"); diff --git a/templates/tools/bash.md b/templates/tools/bash.md index db711645..12f52af3 100644 --- a/templates/tools/bash.md +++ b/templates/tools/bash.md @@ -30,7 +30,9 @@ Usage notes: - The command argument is required. - The sideEffects argument is required. Declare the minimum permission scopes the command may need. - You can use `run_in_background: true` to run a command in the background. Only use this if you need to perform a blocking task, like running a server for the upcoming test scripts. - - When using `run_in_background`, do not add `&` to the command. Output is written to a log file. + - When using `run_in_background`, do NOT add `&` to the command. Output is written to a log file. + - Before your final response, stop background tasks that has not reported a completed state, unless the user explicitly asks to keep it running. + - To stop a background command, use the `stopCommand` returned in the tool result metadata. - Use `sideEffects: []` only for commands that do not read, write, delete, query Git history, mutate Git history, or access the network, such as `date` or `node --version`. - Use `*-out-cwd` when the command accesses paths outside the current workspace. For example, `cat /etc/hosts` requires `["read-out-cwd"]`. - Use `query-git-log` for commands such as `git log`, `git show HEAD`, `git blame`, or history diffs. Use `mutate-git-log` for commands such as `git commit`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, or `git tag`. From de997c69b6a88c1295808a1fde8c6272b99e9f6e Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 2 Jun 2026 15:04:29 +0800 Subject: [PATCH 124/212] feat: enhance cursor handling in PromptInput component to fix IME composition anchoring --- src/ui/views/PromptInput.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index b812a73d..ab2974e8 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -46,9 +46,11 @@ import { readClipboardImageAsync } from "../core/clipboard"; import { useTerminalInput, usePasteHandling, useHistoryNavigation } from "../hooks"; import type { InputKey } from "../hooks"; import { + getPromptCursorPlacement, useHiddenTerminalCursor, useTerminalExtendedKeys, useBracketedPaste, + usePromptTerminalCursor, useTerminalFocusReporting, } from "../hooks"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; @@ -198,10 +200,20 @@ export const PromptInput = React.memo(function PromptInput({ ? `${loadingText}${processOrPasteHint}` : `esc to interrupt · ctrl+c to cancel input${processOrPasteHint}` : `enter send · shift+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${processOrPasteHint}`; + const showFooterText = useMemo( + () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, + [showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu] + ); + const cursorPlacement = useMemo( + () => getPromptCursorPlacement(buffer, screenWidth, 2, footerText), + [buffer, footerText, screenWidth] + ); + const usePositionedCursor = !disabled && hasTerminalFocus && !showFooterText; useTerminalFocusReporting(stdout, !disabled); useTerminalExtendedKeys(stdout, !disabled); useBracketedPaste(stdout, !disabled); - useHiddenTerminalCursor(stdout, !disabled); + usePromptTerminalCursor(stdout, cursorPlacement, usePositionedCursor); + useHiddenTerminalCursor(stdout, !disabled && !usePositionedCursor); const refreshFileMentionItems = React.useCallback(() => { setFileMentionItems(scanFileMentionItems(projectRoot)); @@ -714,11 +726,6 @@ export const PromptInput = React.memo(function PromptInput({ clearUndoRedoStacks(); } - const showFooterText = useMemo( - () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, - [showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu] - ); - const matchedCommand = slashToken ? findExactSlashCommand(slashItems, slashToken) : null; const inlineHint = matchedCommand?.args ? ` ${matchedCommand.args.join(ARGS_SEPARATOR)}` : ""; From 1eaeb3e1b46dc07e28fc7fc4857e631a5a49dcab Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 3 Jun 2026 09:25:05 +0800 Subject: [PATCH 125/212] feat: update karpathy-guidelines.md --- templates/skills/karpathy-guidelines.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/skills/karpathy-guidelines.md b/templates/skills/karpathy-guidelines.md index c134d5e3..ae47e9da 100644 --- a/templates/skills/karpathy-guidelines.md +++ b/templates/skills/karpathy-guidelines.md @@ -10,6 +10,8 @@ Behavioral guidelines to reduce common LLM coding mistakes. **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. +**Internal use:** Apply these guidelines silently. Do not cite this document, its title, or guideline names in user-facing responses. + ## 1. Think Before Coding **Don't assume. Don't hide confusion. Surface tradeoffs.** @@ -64,4 +66,4 @@ For multi-step tasks, state a brief plan: 3. [Step] → verify: [check] ``` -Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. \ No newline at end of file +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. From bdfccca5338d40598c30468be5a886d69e3160db Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 3 Jun 2026 17:00:15 +0800 Subject: [PATCH 126/212] feat: improve the Markdown underscore rendering --- src/tests/markdown.test.ts | 15 ++++++++++++++ src/ui/components/MessageView/markdown.ts | 25 +++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/tests/markdown.test.ts b/src/tests/markdown.test.ts index bc5d33cb..73e2f93c 100644 --- a/src/tests/markdown.test.ts +++ b/src/tests/markdown.test.ts @@ -42,6 +42,21 @@ test("renderMarkdown styles inline code without removing it", () => { assert.equal(result.includes("npm install"), true); }); +test("renderMarkdown preserves underscores inside inline code", () => { + const source = + "Use `redo_completed_tasks2_1min`, replace `execute_query` with `select_one`/`select_all`, and check `ocr_result`."; + const result = stripAnsi(renderMarkdown(source)); + assert.equal( + result, + "Use redo_completed_tasks2_1min, replace execute_query with select_one/select_all, and check ocr_result." + ); +}); + +test("renderMarkdown preserves underscores in plain identifiers", () => { + const result = stripAnsi(renderMarkdown("Check redo_completed_tasks2_1min and ocr_result values.")); + assert.equal(result, "Check redo_completed_tasks2_1min and ocr_result values."); +}); + test("renderMarkdown keeps bullet markers", () => { const result = stripAnsi(renderMarkdown("- item one\n- item two")); assert.equal(result.includes("- item one"), true); diff --git a/src/ui/components/MessageView/markdown.ts b/src/ui/components/MessageView/markdown.ts index 3ebb58ba..0b012642 100644 --- a/src/ui/components/MessageView/markdown.ts +++ b/src/ui/components/MessageView/markdown.ts @@ -396,10 +396,31 @@ function renderInlineLine(line: string): string { function renderInlineSpans(text: string): string { if (!text) return text; + + const parts: string[] = []; + const codeRe = /`([^`]+)`/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = codeRe.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(renderEmphasisSpans(text.slice(lastIndex, match.index))); + } + parts.push(chalk.cyan(match[1] ?? "")); + lastIndex = match.index + match[0].length; + } + + if (lastIndex < text.length) { + parts.push(renderEmphasisSpans(text.slice(lastIndex))); + } + + return parts.join(""); +} + +function renderEmphasisSpans(text: string): string { let result = text; - result = result.replace(/`([^`]+)`/g, (_, inner) => chalk.cyan(inner)); result = result.replace(/\*\*([^*]+)\*\*/g, (_, inner) => chalk.bold(inner)); result = result.replace(/(? chalk.italic(inner)); - result = result.replace(/_([^_\n]+)_/g, (_, inner) => chalk.italic(inner)); + result = result.replace(/(? chalk.italic(inner)); return result; } From 9609d268749b6a7287337f932ede180585295f0b Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 4 Jun 2026 10:33:45 +0800 Subject: [PATCH 127/212] feat: replace useEffect with useLayoutEffect for better performance in useTerminalInput and PromptInput components --- src/ui/hooks/useTerminalInput.ts | 6 +++--- src/ui/views/PromptInput.tsx | 19 +++++++------------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/ui/hooks/useTerminalInput.ts b/src/ui/hooks/useTerminalInput.ts index e3d63491..9e985fa9 100644 --- a/src/ui/hooks/useTerminalInput.ts +++ b/src/ui/hooks/useTerminalInput.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useLayoutEffect, useRef } from "react"; import { useStdin } from "ink"; export type InputKey = { @@ -249,7 +249,7 @@ export function useTerminalInput( // O(n²) copying when the terminal splits a large paste across many events. const pasteRef = useRef({ active: false, chunks: [] as string[] }); - useEffect(() => { + useLayoutEffect(() => { if (!isActive) { pasteRef.current.active = false; pasteRef.current.chunks = []; @@ -261,7 +261,7 @@ export function useTerminalInput( }; }, [isActive, setRawMode]); - useEffect(() => { + useLayoutEffect(() => { if (!isActive) { return; } diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index ab2974e8..19342da5 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -46,11 +46,9 @@ import { readClipboardImageAsync } from "../core/clipboard"; import { useTerminalInput, usePasteHandling, useHistoryNavigation } from "../hooks"; import type { InputKey } from "../hooks"; import { - getPromptCursorPlacement, useHiddenTerminalCursor, useTerminalExtendedKeys, useBracketedPaste, - usePromptTerminalCursor, useTerminalFocusReporting, } from "../hooks"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; @@ -204,16 +202,9 @@ export const PromptInput = React.memo(function PromptInput({ () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, [showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu] ); - const cursorPlacement = useMemo( - () => getPromptCursorPlacement(buffer, screenWidth, 2, footerText), - [buffer, footerText, screenWidth] - ); - const usePositionedCursor = !disabled && hasTerminalFocus && !showFooterText; - useTerminalFocusReporting(stdout, !disabled); - useTerminalExtendedKeys(stdout, !disabled); - useBracketedPaste(stdout, !disabled); - usePromptTerminalCursor(stdout, cursorPlacement, usePositionedCursor); - useHiddenTerminalCursor(stdout, !disabled && !usePositionedCursor); + // The prompt draws its own inverse-video cursor inside the text. Keep the + // native terminal cursor hidden so wrapping edges do not show two cursors. + const hideNativeCursor = !disabled; const refreshFileMentionItems = React.useCallback(() => { setFileMentionItems(scanFileMentionItems(projectRoot)); @@ -569,6 +560,10 @@ export const PromptInput = React.memo(function PromptInput({ }, { isActive: !disabled } ); + useTerminalFocusReporting(stdout, !disabled); + useTerminalExtendedKeys(stdout, !disabled); + useBracketedPaste(stdout, !disabled); + useHiddenTerminalCursor(stdout, hideNativeCursor); function undo(): void { const previous = undoPromptEdit(undoRedoRef.current, buffer); From 11aa194edbe14778cf13f1f5c958dd5f8d701be8 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 4 Jun 2026 11:28:44 +0800 Subject: [PATCH 128/212] feat: add MCP tool name handling with API-safe names --- src/mcp/mcp-manager.ts | 77 +++++++++++++++++++++++++++++++++++++-- src/tests/session.test.ts | 58 +++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/src/mcp/mcp-manager.ts b/src/mcp/mcp-manager.ts index fe8066b4..6d2edc63 100644 --- a/src/mcp/mcp-manager.ts +++ b/src/mcp/mcp-manager.ts @@ -1,3 +1,4 @@ +import { createHash } from "crypto"; import { McpClient, type McpToolDefinition, type McpPromptDefinition, type McpResourceDefinition } from "./mcp-client"; import type { McpServerConfig } from "../settings"; @@ -5,6 +6,8 @@ const MCP_STARTUP_TIMEOUT_MS = process.env.DEEPCODE_MCP_TIMEOUT ? parseInt(process.env.DEEPCODE_MCP_TIMEOUT, 10) : 30_000; const MCP_CALL_TOOL_TIMEOUT_MS = 60_000; +const API_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/; +const API_TOOL_NAME_MAX_LENGTH = 64; type McpToolEntry = { serverName: string; @@ -27,6 +30,32 @@ export type McpServerStatus = { resources: string[]; }; +function buildMcpNamespacedName( + serverName: string, + toolName: string, + usedNames: ReadonlySet = new Set() +): string { + const rawName = buildRawMcpNamespacedName(serverName, toolName); + const sanitizedName = `mcp__${sanitizeApiToolNamePart(serverName)}__${sanitizeApiToolNamePart(toolName)}`; + let candidate = fitApiToolName(sanitizedName, rawName); + if (!usedNames.has(candidate)) { + return candidate; + } + + const hash = hashToolName(rawName); + candidate = fitApiToolNameWithSuffix(sanitizedName, `_${hash}`); + if (!usedNames.has(candidate)) { + return candidate; + } + + for (let index = 2; ; index += 1) { + candidate = fitApiToolNameWithSuffix(sanitizedName, `_${hash}_${index}`); + if (!usedNames.has(candidate)) { + return candidate; + } + } +} + export class McpManager { private clients: McpClient[] = []; private tools: McpToolEntry[] = []; @@ -151,8 +180,10 @@ export class McpManager { const serverTools = await client.listTools(MCP_STARTUP_TIMEOUT_MS); if (this.disposed) return; const toolNamespacedNames: string[] = []; + const usedToolNames = new Set(this.tools.map((tool) => tool.namespacedName)); for (const tool of serverTools) { - const namespacedName = `mcp__${name}__${tool.name}`; + const namespacedName = buildMcpNamespacedName(name, tool.name, usedToolNames); + usedToolNames.add(namespacedName); this.tools.push({ serverName: name, originalName: tool.name, @@ -289,7 +320,7 @@ export class McpManager { type: "function" as const, function: { name: t.namespacedName, - description: t.definition.description ?? `${t.serverName}: ${t.originalName}`, + description: this.buildMcpToolDescription(t), parameters: { type: "object" as const, properties: t.definition.inputSchema.properties, @@ -413,8 +444,10 @@ export class McpManager { const serverTools = await client.listTools(MCP_STARTUP_TIMEOUT_MS); this.tools = this.tools.filter((t) => t.serverName !== serverName); const toolNamespacedNames: string[] = []; + const usedToolNames = new Set(this.tools.map((tool) => tool.namespacedName)); for (const tool of serverTools) { - const namespacedName = `mcp__${serverName}__${tool.name}`; + const namespacedName = buildMcpNamespacedName(serverName, tool.name, usedToolNames); + usedToolNames.add(namespacedName); this.tools.push({ serverName, originalName: tool.name, @@ -450,4 +483,42 @@ export class McpManager { } this.onStatusChanged?.(); } + + private buildMcpToolDescription(tool: McpToolEntry): string { + const description = tool.definition.description?.trim(); + const source = `${tool.serverName}: ${tool.originalName}`; + if (!description) { + return source; + } + if (tool.namespacedName === buildRawMcpNamespacedName(tool.serverName, tool.originalName)) { + return description; + } + return `${description}\nMCP source: ${source}`; + } +} + +function buildRawMcpNamespacedName(serverName: string, toolName: string): string { + return `mcp__${serverName}__${toolName}`; +} + +function sanitizeApiToolNamePart(value: string): string { + const sanitized = value.replace(/[^a-zA-Z0-9_-]/g, "_"); + return sanitized || "unnamed"; +} + +function fitApiToolName(name: string, rawName: string): string { + if (API_TOOL_NAME_PATTERN.test(name) && name.length <= API_TOOL_NAME_MAX_LENGTH) { + return name; + } + return fitApiToolNameWithSuffix(name, `_${hashToolName(rawName)}`); +} + +function fitApiToolNameWithSuffix(name: string, suffix: string): string { + const maxPrefixLength = API_TOOL_NAME_MAX_LENGTH - suffix.length; + const prefix = name.slice(0, Math.max(1, maxPrefixLength)); + return `${prefix}${suffix}`; +} + +function hashToolName(value: string): string { + return createHash("sha256").update(value).digest("hex").slice(0, 8); } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index b02642bb..15769dff 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -542,6 +542,64 @@ rl.on("line", (line) => { assert.deepEqual(manager.getMcpStatus(), []); }); +test("SessionManager exposes MCP tools with API-safe names and preserves original dispatch names", async () => { + const workspace = createTempDir("deepcode-mcp-safe-name-workspace-"); + const serverPath = path.join(workspace, "mcp-invalid-name-server.cjs"); + fs.writeFileSync( + serverPath, + ` +const readline = require("readline"); +const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); +function send(message) { + process.stdout.write(JSON.stringify(message) + "\\n"); +} +rl.on("line", (line) => { + const request = JSON.parse(line); + if (!("id" in request)) { + return; + } + if (request.method === "initialize") { + send({ jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} } } }); + return; + } + if (request.method === "tools/list") { + send({ jsonrpc: "2.0", id: request.id, result: { tools: [ + { name: "speak.text", description: "Speak text", inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] } }, + { name: "speak/text", description: "Speak text using a slash name", inputSchema: { type: "object", properties: {} } } + ] } }); + return; + } + if (request.method === "tools/call") { + send({ jsonrpc: "2.0", id: request.id, result: { content: [{ type: "text", text: request.params.name + ":" + (request.params.arguments.text || "") }] } }); + return; + } + send({ jsonrpc: "2.0", id: request.id, result: { content: [] } }); +}); +`, + "utf8" + ); + + const manager = createSessionManager(workspace, "machine-id-mcp-safe-name"); + await manager.initMcpServers({ "voice.box": { command: process.execPath, args: [serverPath] } }); + + const status = manager.getMcpStatus()[0]; + assert.equal(status?.status, "ready"); + assert.deepEqual(status?.tools, ["mcp__voice_box__speak_text", "mcp__voice_box__speak_text_59a610ad"]); + + const mcpManager = (manager as any).mcpManager; + const definitions = mcpManager.getMcpToolDefinitions(); + assert.equal(definitions[0].function.name, "mcp__voice_box__speak_text"); + assert.match(definitions[0].function.name, /^[a-zA-Z0-9_-]+$/); + assert.match(definitions[0].function.description, /MCP source: voice\.box: speak\.text/); + assert.deepEqual(await mcpManager.executeMcpTool("mcp__voice_box__speak_text", { text: "ok" }), { + ok: true, + name: "mcp__voice_box__speak_text", + output: "speak.text:ok", + }); + + manager.dispose(); +}); + test("SessionManager dispose kills live processes without timeout controls", (t) => { if (process.platform === "win32") { t.skip("process group kill assertion is non-Windows specific"); From fe3b8bd4b8aa73a9930226b075b036a9721150cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 05:01:46 +0000 Subject: [PATCH 129/212] chore(deps): bump ws from 8.20.0 to 8.21.0 Bumps [ws](https://github.com/websockets/ws) from 8.20.0 to 8.21.0. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.20.0...8.21.0) --- updated-dependencies: - dependency-name: ws dependency-version: 8.21.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d354378d..a7bd6dde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4214,9 +4214,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" From 17e39d3f3d49c88e626243d1e9b61b313fd91b6b Mon Sep 17 00:00:00 2001 From: ZHANGFEI23 Date: Thu, 4 Jun 2026 16:42:19 +0800 Subject: [PATCH 130/212] feat: session name can be edited now --- src/session.ts | 21 +++++++ src/ui/views/App.tsx | 8 +++ src/ui/views/SessionList.tsx | 109 ++++++++++++++++++++++++++++++++--- 3 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/session.ts b/src/session.ts index f12b91f2..d37b6ef6 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1671,6 +1671,27 @@ ${skillMd} return true; } + /** + * Rename a session by updating its summary (display title). + * Returns true if the session was found and renamed, false otherwise. + */ + renameSession(sessionId: string, summary: string): boolean { + const trimmed = summary.trim(); + if (!trimmed) { + return false; + } + const entry = this.getSession(sessionId); + if (!entry) { + return false; + } + this.updateSessionEntry(sessionId, (existing) => ({ + ...existing, + summary: trimmed, + updateTime: new Date().toISOString(), + })); + return true; + } + listSessionMessages(sessionId: string): SessionMessage[] { const messagePath = this.getSessionMessagesPath(sessionId); if (!fs.existsSync(messagePath)) { diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index f07da6f5..1579848f 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -751,6 +751,14 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl onDelete={(id) => { void handleDeleteSession(id); }} + onRename={(id, newName) => { + if (sessionManager.renameSession(id, newName)) { + refreshSessionsList(); + setStatusLine(`Session renamed to "${newName}".`); + } else { + setErrorLine("Failed to rename session."); + } + }} /> ) : view === "undo" ? ( void; onCancel: () => void; onDelete?: (sessionId: string) => void; + onRename?: (sessionId: string, newName: string) => void; }; /** @@ -38,10 +39,13 @@ export function filterSessions(sessions: SessionEntry[], query: string): Session }); } -export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): React.ReactElement { +export function SessionList({ sessions, onSelect, onCancel, onDelete, onRename }: Props): React.ReactElement { const [index, setIndex] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [confirmDeleteSessionId, setConfirmDeleteSessionId] = useState(null); + const [renameSessionId, setRenameSessionId] = useState(null); + const [renameValue, setRenameValue] = useState(""); + const [renameCursor, setRenameCursor] = useState(0); const { columns, rows } = useWindowSize(); // Filter sessions by search query @@ -83,6 +87,65 @@ export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): const selectedSession = filteredSessions[safeIndex]; useInput((input, key) => { + // If in rename mode, handle rename editing + if (renameSessionId) { + if (key.return) { + if (renameValue.trim()) { + onRename?.(renameSessionId, renameValue.trim()); + } + setRenameSessionId(null); + setRenameValue(""); + setRenameCursor(0); + return; + } + if (key.escape) { + setRenameSessionId(null); + setRenameValue(""); + setRenameCursor(0); + return; + } + if (key.leftArrow) { + setRenameCursor((c) => Math.max(0, c - 1)); + return; + } + if (key.rightArrow) { + setRenameCursor((c) => Math.min(renameValue.length, c + 1)); + return; + } + if (key.home) { + setRenameCursor(0); + return; + } + if (key.end) { + setRenameCursor(renameValue.length); + return; + } + if (key.delete) { + if (renameCursor < renameValue.length) { + setRenameValue((prev) => prev.slice(0, renameCursor) + prev.slice(renameCursor + 1)); + // cursor stays at same position (next char shifts left) + } + return; + } + if (key.backspace) { + if (renameCursor > 0) { + setRenameValue((prev) => prev.slice(0, renameCursor - 1) + prev.slice(renameCursor)); + setRenameCursor((c) => c - 1); + } + return; + } + // Printable character: insert at cursor position + if (input && input.length > 0 && !key.meta && !key.ctrl && !key.tab) { + if (key.upArrow || key.downArrow) { + return; + } + setRenameValue((prev) => prev.slice(0, renameCursor) + input + prev.slice(renameCursor)); + setRenameCursor((c) => c + input.length); + return; + } + return; + } + // If in delete confirmation mode, handle confirm/cancel if (confirmDeleteSessionId) { if (key.return) { @@ -114,6 +177,17 @@ export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): return; } + // Ctrl+R: start rename on selected session + if (key.ctrl && (input === "r" || input === "R")) { + if (selectedSession && onRename) { + const name = selectedSession.summary || ""; + setRenameSessionId(selectedSession.id); + setRenameValue(name); + setRenameCursor(name.length); + return; + } + } + // Delete key: remove search character, or start delete confirmation if (key.delete || key.backspace) { if (searchQuery) { @@ -237,6 +311,7 @@ export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): const actualIndex = scrollOffset + i; const isSelected = actualIndex === safeIndex; const isConfirming = confirmDeleteSessionId === session.id; + const isRenaming = renameSessionId === session.id; return ( @@ -244,12 +319,20 @@ export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): - - {formatSessionTitle(session.summary || "Untitled")} - + {isRenaming ? ( + + Rename: {renameValue.slice(0, renameCursor)} + | + {renameValue.slice(renameCursor)} + + ) : ( + + {formatSessionTitle(session.summary || "Untitled")} + + )} {isConfirming ? ( [Delete? Enter=yes, Esc=no] - ) : ( + ) : isRenaming ? null : ( ({formatSessionStatus(session.status)}) )} @@ -272,7 +355,19 @@ export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): {/* Footer */} - {confirmDeleteSessionId ? ( + {renameSessionId ? ( + + Input new session name, + + Enter + + to save · + + Esc + + to cancel + + ) : confirmDeleteSessionId ? ( Delete this session? @@ -292,7 +387,7 @@ export function SessionList({ sessions, onSelect, onCancel, onDelete }: Props): ) : ( - Type to search · ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel · Del delete + Type to search · ↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel · Del delete · Ctrl+r rename )} From 285ae56fca6150174aeee702f37cd2b5796360e5 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 4 Jun 2026 17:30:06 +0800 Subject: [PATCH 131/212] feat: enhance update handling and success message --- src/cli.tsx | 7 ++++--- src/common/update-check.ts | 6 ++---- src/tests/update-check.test.ts | 6 +++++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cli.tsx b/src/cli.tsx index 87fb9fb5..56ebdb6f 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -78,6 +78,9 @@ void main(); async function main(): Promise { const updatePromptResult = await promptForPendingUpdate(packageInfo); + if (updatePromptResult.installed) { + process.exit(0); + } const restartRef: { current: (() => void) | null } = { current: null }; @@ -110,9 +113,7 @@ async function main(): Promise { }); } - if (!updatePromptResult.installed) { - void checkForNpmUpdate(packageInfo); - } + void checkForNpmUpdate(packageInfo); startApp(); } diff --git a/src/common/update-check.ts b/src/common/update-check.ts index 09c0273c..2d27c7a6 100644 --- a/src/common/update-check.ts +++ b/src/common/update-check.ts @@ -4,7 +4,6 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { render, type Instance } from "ink"; -import chalk from "chalk"; import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; import { killProcessTree } from "./process-tree"; @@ -27,6 +26,7 @@ const UPDATE_STATE_FILE = "update-check.json"; const NPM_VIEW_TIMEOUT_MS = 5000; const MAX_NPM_VIEW_OUTPUT_CHARS = 64 * 1024; const TENCENT_MIRROR_REGISTRY = "https://mirrors.cloud.tencent.com/npm/"; +export const UPDATE_SUCCESS_MESSAGE = "🎉 Update ran successfully! Please restart Deep Code."; export async function promptForPendingUpdate(packageInfo: PackageInfo): Promise<{ installed: boolean }> { const state = readUpdateState(); @@ -57,9 +57,7 @@ export async function promptForPendingUpdate(packageInfo: PackageInfo): Promise< const ok = await runNpmInstallGlobal(installSpec); if (ok) { writeUpdateState({ ...state, pending: null }); - process.stdout.write( - `\n${chalk.red("Deep Code has been updated. Please restart the CLI to use the new version.")}\n\n` - ); + process.stdout.write(`${UPDATE_SUCCESS_MESSAGE}\n\n`); } return { installed: ok }; } diff --git a/src/tests/update-check.test.ts b/src/tests/update-check.test.ts index 93b30360..34e85912 100644 --- a/src/tests/update-check.test.ts +++ b/src/tests/update-check.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { compareVersions, parseNpmViewVersion } from "../common/update-check"; +import { UPDATE_SUCCESS_MESSAGE, compareVersions, parseNpmViewVersion } from "../common/update-check"; test("compareVersions orders semantic versions", () => { assert.equal(compareVersions("0.1.4", "0.1.3"), 1); @@ -14,3 +14,7 @@ test("parseNpmViewVersion parses npm view JSON and plain output", () => { assert.equal(parseNpmViewVersion("0.1.5\n"), "0.1.5"); assert.equal(parseNpmViewVersion("\n"), null); }); + +test("UPDATE_SUCCESS_MESSAGE tells the user to restart Deep Code", () => { + assert.equal(UPDATE_SUCCESS_MESSAGE, "🎉 Update ran successfully! Please restart Deep Code."); +}); From fdbfea4a99890b827d3609c9a90bfb409ddb2412 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 4 Jun 2026 18:05:18 +0800 Subject: [PATCH 132/212] feat: implement temperature support for settings.json --- docs/configuration.md | 2 ++ docs/configuration_en.md | 4 +++- src/common/openai-client.ts | 4 ++++ src/session.ts | 11 ++++++---- src/settings.ts | 19 +++++++++++++++++ src/tests/session.test.ts | 2 ++ src/tests/settings-and-notify.test.ts | 30 ++++++++++++++++++++++++++- src/tools/executor.ts | 1 + 8 files changed, 67 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 922f39e3..752f7ab7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -35,6 +35,7 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 | `notify` | string | 任务完成通知脚本的完整路径(如 Slack 通知脚本) | | `webSearchTool` | string | 自定义联网搜索脚本的完整路径 | | `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) | +| `temperature` | number | 模型采样温度,范围 `0` 到 `2` | #### `env` 子字段 @@ -43,6 +44,7 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 | `MODEL` | string | 模型名称。例如 `"deepseek-v4-pro"`、`"deepseek-v4-flash"` | | `BASE_URL` | string | API 请求的基础 URL。例如 `"https://api.deepseek.com"` | | `API_KEY` | string | API 密钥 | +| `TEMPERATURE` | string | Chat Completions 采样温度,范围 `"0"` 到 `"2"` | | `THINKING_ENABLED` | string | 是否启用思考模式 | | `REASONING_EFFORT` | string | 推理强度 | | `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 | diff --git a/docs/configuration_en.md b/docs/configuration_en.md index f53fb114..8634992c 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -35,6 +35,7 @@ The following are all the top-level fields supported in `settings.json`, along w | `notify` | string | Full path to a task-completion notification script (e.g., Slack notification script) | | `webSearchTool` | string | Full path to a custom web search script | | `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) | +| `temperature` | number | Sampling temperature for LLM, from `0` to `2` | #### `env` Sub-fields @@ -43,6 +44,7 @@ The following are all the top-level fields supported in `settings.json`, along w | `MODEL` | string | Model name, e.g. `"deepseek-v4-pro"`, `"deepseek-v4-flash"` | | `BASE_URL` | string | Base URL for API requests, e.g. `"https://api.deepseek.com"` | | `API_KEY` | string | API key | +| `TEMPERATURE` | string | Sampling temperature for chat completions, from `"0"` to `"2"` | | `THINKING_ENABLED`| string | Enable thinking mode | | `REASONING_EFFORT`| string | Reasoning intensity | | `DEBUG_LOG_ENABLED`| string| Enable debug log output | @@ -193,4 +195,4 @@ Applied in the following priority order (lower-numbered overridden by higher-num 2. User-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}` 3. Project-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}` 4. Project-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}` -5. System environment variable: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode` \ No newline at end of file +5. System environment variable: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode` diff --git a/src/common/openai-client.ts b/src/common/openai-client.ts index 35877835..d3b56c08 100644 --- a/src/common/openai-client.ts +++ b/src/common/openai-client.ts @@ -23,6 +23,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { client: OpenAI | null; model: string; baseURL: string; + temperature?: number; thinkingEnabled: boolean; reasoningEffort: "high" | "max"; debugLogEnabled: boolean; @@ -38,6 +39,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { client: null, model: settings.model, baseURL: settings.baseURL, + temperature: settings.temperature, thinkingEnabled: settings.thinkingEnabled, reasoningEffort: settings.reasoningEffort, debugLogEnabled: settings.debugLogEnabled, @@ -55,6 +57,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { client: cachedOpenAI, model: settings.model, baseURL: settings.baseURL, + temperature: settings.temperature, thinkingEnabled: settings.thinkingEnabled, reasoningEffort: settings.reasoningEffort, debugLogEnabled: settings.debugLogEnabled, @@ -91,6 +94,7 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { client: cachedOpenAI, model: settings.model, baseURL: settings.baseURL, + temperature: settings.temperature, thinkingEnabled: settings.thinkingEnabled, reasoningEffort: settings.reasoningEffort, debugLogEnabled: settings.debugLogEnabled, diff --git a/src/session.ts b/src/session.ts index d37b6ef6..1023a624 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1197,7 +1197,7 @@ ${skillMd} permissionPrompt?: UserPromptContent ): Promise { const startedAt = Date.now(); - const { client, model, baseURL, thinkingEnabled, reasoningEffort, debugLogEnabled, notify, env } = + const { client, model, baseURL, temperature, thinkingEnabled, reasoningEffort, debugLogEnabled, notify, env } = this.createOpenAIClient(); const now = new Date().toISOString(); rebuildSessionStateFromHistory(sessionId, this.listSessionMessages(sessionId)); @@ -1300,6 +1300,7 @@ ${skillMd} client, { model, + ...(temperature !== undefined ? { temperature } : {}), messages, tools: getTools(this.getPromptToolOptions(), this.mcpToolDefinitions), ...thinkingOptions, @@ -1310,7 +1311,7 @@ ${skillMd} enabled: debugLogEnabled, location: "SessionManager.activateSession", baseURL, - params: { iteration, thinkingEnabled, reasoningEffort }, + params: { iteration, temperature, thinkingEnabled, reasoningEffort }, } ); @@ -1440,7 +1441,8 @@ ${skillMd} async compactSession(sessionId: string, signal?: AbortSignal): Promise { this.throwIfAborted(signal); - const { client, model, baseURL, thinkingEnabled, reasoningEffort, debugLogEnabled } = this.createOpenAIClient(); + const { client, model, baseURL, temperature, thinkingEnabled, reasoningEffort, debugLogEnabled } = + this.createOpenAIClient(); if (!client) { return; } @@ -1472,6 +1474,7 @@ ${skillMd} client, { model, + ...(temperature !== undefined ? { temperature } : {}), messages: [{ role: "user", content: compactPrompt }], ...thinkingOptions, }, @@ -1481,7 +1484,7 @@ ${skillMd} enabled: debugLogEnabled, location: "SessionManager.compactSession", baseURL, - params: { thinkingEnabled, reasoningEffort }, + params: { temperature, thinkingEnabled, reasoningEffort }, } ); this.throwIfAborted(signal); diff --git a/src/settings.ts b/src/settings.ts index 14755dd1..deb90344 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -7,6 +7,7 @@ export type DeepcodingEnv = Record & { MODEL?: string; BASE_URL?: string; API_KEY?: string; + TEMPERATURE?: string; THINKING_ENABLED?: string; REASONING_EFFORT?: string; DEBUG_LOG_ENABLED?: string; @@ -45,6 +46,7 @@ export type PermissionSettings = { export type DeepcodingSettings = { env?: DeepcodingEnv; model?: string; + temperature?: number; thinkingEnabled?: boolean; reasoningEffort?: ReasoningEffort; debugLogEnabled?: boolean; @@ -60,6 +62,7 @@ export type ResolvedDeepcodingSettings = { apiKey?: string; baseURL: string; model: string; + temperature?: number; thinkingEnabled: boolean; reasoningEffort: ReasoningEffort; debugLogEnabled: boolean; @@ -100,6 +103,14 @@ function parseBoolean(value: unknown): boolean | undefined { return undefined; } +function parseTemperature(value: unknown): number | undefined { + const raw = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : NaN; + if (!Number.isFinite(raw) || raw < 0 || raw > 2) { + return undefined; + } + return raw; +} + function trimString(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } @@ -308,6 +319,13 @@ export function resolveSettingsSources( resolveReasoningEffort(userEnv.REASONING_EFFORT) ?? "max"; + const temperature = + parseTemperature(systemEnv.TEMPERATURE) ?? + parseTemperature(projectSettings?.temperature) ?? + parseTemperature(projectEnv.TEMPERATURE) ?? + parseTemperature(userSettings?.temperature) ?? + parseTemperature(userEnv.TEMPERATURE); + const debugLogEnabled = parseBoolean(systemEnv.DEBUG_LOG_ENABLED) ?? parseBoolean(projectSettings?.debugLogEnabled) ?? @@ -337,6 +355,7 @@ export function resolveSettingsSources( apiKey: trimString(env.API_KEY) || undefined, baseURL: trimString(env.BASE_URL) || defaults.baseURL, model, + temperature, thinkingEnabled, reasoningEffort, debugLogEnabled, diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 15769dff..1d1cf725 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -2759,6 +2759,7 @@ test("SessionManager streams chat completions and counts reasoning progress", as create: async (request: Record) => { assert.equal(request.stream, true); assert.deepEqual(request.stream_options, { include_usage: true }); + assert.equal(request.temperature, 0.25); return createChatStreamResponse([ { choices: [{ delta: { reasoning_content: "思考" } }] }, { choices: [{ delta: { content: "hello" } }] }, @@ -2782,6 +2783,7 @@ test("SessionManager streams chat completions and counts reasoning progress", as client: client as any, model: "test-model", baseURL: "https://api.deepseek.com", + temperature: 0.25, thinkingEnabled: false, }), getResolvedSettings: () => ({ model: "test-model" }), diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts index 9e18dc1c..5bc81cc9 100644 --- a/src/tests/settings-and-notify.test.ts +++ b/src/tests/settings-and-notify.test.ts @@ -19,6 +19,7 @@ test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool BASE_URL: "https://example.com/v1", API_KEY: "sk-test", }, + temperature: 0.3, thinkingEnabled: true, reasoningEffort: "high", debugLogEnabled: true, @@ -35,6 +36,7 @@ test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool assert.equal(resolved.model, "deepseek-v3.2"); assert.equal(resolved.baseURL, "https://example.com/v1"); assert.equal(resolved.apiKey, "sk-test"); + assert.equal(resolved.temperature, 0.3); assert.equal(resolved.thinkingEnabled, true); assert.equal(resolved.reasoningEffort, "high"); assert.equal(resolved.debugLogEnabled, true); @@ -60,10 +62,11 @@ test("resolveSettings gives top-level model priority over env MODEL", () => { assert.equal(resolved.model, "deepseek-v4-flash"); }); -test("resolveSettings reads THINKING_ENABLED, REASONING_EFFORT, and DEBUG_LOG_ENABLED from env", () => { +test("resolveSettings reads TEMPERATURE, THINKING_ENABLED, REASONING_EFFORT, and DEBUG_LOG_ENABLED from env", () => { const resolved = resolveSettings( { env: { + TEMPERATURE: "0.7", THINKING_ENABLED: "true", REASONING_EFFORT: "high", DEBUG_LOG_ENABLED: "true", @@ -77,6 +80,7 @@ test("resolveSettings reads THINKING_ENABLED, REASONING_EFFORT, and DEBUG_LOG_EN ); assert.equal(resolved.thinkingEnabled, true); + assert.equal(resolved.temperature, 0.7); assert.equal(resolved.reasoningEffort, "high"); assert.equal(resolved.debugLogEnabled, true); assert.equal(resolved.model, "default-model"); @@ -138,12 +142,14 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre MODEL: "user-env-model", THINKING_ENABLED: "false", REASONING_EFFORT: "high", + TEMPERATURE: "0.2", DEBUG_LOG_ENABLED: "false", WEBHOOK: "user-webhook", }, model: "user-top-model", thinkingEnabled: true, reasoningEffort: "max", + temperature: 0.4, debugLogEnabled: true, telemetryEnabled: false, }, @@ -153,9 +159,11 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre MODEL: "project-env-model", THINKING_ENABLED: "false", DEBUG_LOG_ENABLED: "false", + TEMPERATURE: "0.6", }, model: "project-top-model", thinkingEnabled: true, + temperature: 0.8, telemetryEnabled: true, }, { @@ -166,6 +174,7 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre DEEPCODE_MODEL: "system-model", DEEPCODE_THINKING_ENABLED: "false", DEEPCODE_REASONING_EFFORT: "high", + DEEPCODE_TEMPERATURE: "1.2", DEEPCODE_DEBUG_LOG_ENABLED: "true", DEEPCODE_TELEMETRY_ENABLED: "false", DEEPCODE_WEBHOOK: "system-webhook", @@ -176,6 +185,7 @@ test("resolveSettingsSources applies user, project, and DEEPCODE environment pre assert.equal(resolved.apiKey, "project-key"); assert.equal(resolved.thinkingEnabled, false); assert.equal(resolved.reasoningEffort, "high"); + assert.equal(resolved.temperature, 1.2); assert.equal(resolved.debugLogEnabled, true); assert.equal(resolved.telemetryEnabled, false); assert.equal(resolved.env.WEBHOOK, "system-webhook"); @@ -341,6 +351,24 @@ test("resolveSettings defaults invalid reasoning effort to max", () => { assert.equal(resolved.reasoningEffort, "max"); }); +test("resolveSettings ignores invalid temperature values", () => { + const resolved = resolveSettings( + { + env: { + TEMPERATURE: "hot", + }, + temperature: 3, + }, + { + model: "default-model", + baseURL: "https://default.example.com", + }, + TEST_PROCESS_ENV + ); + + assert.equal(resolved.temperature, undefined); +}); + test("applyModelConfigSelection writes model only when the effective model changes or already exists", () => { const result = applyModelConfigSelection( { diff --git a/src/tools/executor.ts b/src/tools/executor.ts index 60b18882..53846f48 100644 --- a/src/tools/executor.ts +++ b/src/tools/executor.ts @@ -13,6 +13,7 @@ export type CreateOpenAIClient = () => { client: OpenAI | null; model: string; baseURL?: string; + temperature?: number; thinkingEnabled: boolean; reasoningEffort?: ReasoningEffort; debugLogEnabled?: boolean; From 36b108b72a94150beea70f2bf6a8538005e226cc Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 4 Jun 2026 18:20:19 +0800 Subject: [PATCH 133/212] feat: identifyMatchingSkillNames always sends temperature: 0.1 in LLM request --- src/session.ts | 3 ++- src/tests/session.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/session.ts b/src/session.ts index 1023a624..37a09c6f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -747,6 +747,7 @@ The candidate skills are as follows:\n\n`; client, { model, + temperature: 0.1, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, @@ -759,7 +760,7 @@ The candidate skills are as follows:\n\n`; enabled: debugLogEnabled, location: "SessionManager.identifyMatchingSkillNames", baseURL, - params: { purpose: "skill-matching" }, + params: { purpose: "skill-matching", temperature: 0.1 }, } ); this.throwIfAborted(options?.signal); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 1d1cf725..a818b23a 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -2826,7 +2826,8 @@ test("SessionManager persists session and user message before skill matching is const client = { chat: { completions: { - create: async (_request: Record, options?: { signal?: AbortSignal }) => { + create: async (request: Record, options?: { signal?: AbortSignal }) => { + assert.equal(request.temperature, 0.1); return new Promise((_resolve, reject) => { const signal = options?.signal; signal?.addEventListener("abort", () => reject(new APIUserAbortError()), { once: true }); From 405367636b439b6025fceeb620f755c0c9621c0e Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 5 Jun 2026 10:20:55 +0800 Subject: [PATCH 134/212] feat: improve the Agent Skills scan hierarchy and priority --- README-en.md | 12 +++++++++--- README-zh_CN.md | 12 +++++++++--- README.md | 12 +++++++++--- src/cli.tsx | 7 ++++--- src/session.ts | 23 ++++++++++++----------- src/tests/session.test.ts | 36 ++++++++++++++++++++++++++---------- 6 files changed, 69 insertions(+), 33 deletions(-) diff --git a/README-en.md b/README-en.md index c1d4acb9..453dd64c 100644 --- a/README-en.md +++ b/README-en.md @@ -54,8 +54,14 @@ For complete configuration details (multi-level priority, environment variables, ### **Skills** Deep Code CLI supports agent skills that allow you to extend the assistant's capabilities: -- **User-level Skills**: discovered and activated from `~/.agents/skills/`. -- **Project-level Skills**: loaded from `./.agents/skills/` for project-specific workflows, with legacy `./.deepcode/skills/` compatibility. +Skills are discovered from these locations, in priority order: + +| Scope | Path | Purpose | +| :------ | :-------------------- | :---------------------------- | +| Project | `./.deepcode/skills/` | Deep Code's native location | +| Project | `./.agents/skills/` | Cross-client interoperability | +| User | `~/.deepcode/skills/` | Deep Code's native location | +| User | `~/.agents/skills/` | Cross-client interoperability | ### **Optimized for DeepSeek** - Specifically tuned for DeepSeek model performance. @@ -200,4 +206,4 @@ If you find this tool helpful, please consider supporting us by: [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls [github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE -[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 \ No newline at end of file +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 diff --git a/README-zh_CN.md b/README-zh_CN.md index 26437563..cde02314 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -53,8 +53,14 @@ npm install -g @vegamo/deepcode-cli ### **Skills** Deep Code CLI 支持 agent skills,允许您扩展助手的能力: -- **User-level Skills**:从 `~/.agents/skills/` 目录中发现并激活 skills。 -- **Project-level Skills**:从 `./.agents/skills/` 目录中加载项目专属 skills,并兼容旧的 `./.deepcode/skills/` 目录。 +Skills 会按以下优先级扫描: + +| Scope | Path | Purpose | +| :------ | :-------------------- | :---------------------------- | +| Project | `./.deepcode/skills/` | Deep Code 原生位置 | +| Project | `./.agents/skills/` | 跨客户端互操作 | +| User | `~/.deepcode/skills/` | Deep Code 原生位置 | +| User | `~/.agents/skills/` | 跨客户端互操作 | ### **为 DeepSeek 优化** - 专门为 DeepSeek 模型性能调优。 @@ -199,4 +205,4 @@ npm link [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls [github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE -[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 \ No newline at end of file +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 diff --git a/README.md b/README.md index 26437563..cde02314 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,14 @@ npm install -g @vegamo/deepcode-cli ### **Skills** Deep Code CLI 支持 agent skills,允许您扩展助手的能力: -- **User-level Skills**:从 `~/.agents/skills/` 目录中发现并激活 skills。 -- **Project-level Skills**:从 `./.agents/skills/` 目录中加载项目专属 skills,并兼容旧的 `./.deepcode/skills/` 目录。 +Skills 会按以下优先级扫描: + +| Scope | Path | Purpose | +| :------ | :-------------------- | :---------------------------- | +| Project | `./.deepcode/skills/` | Deep Code 原生位置 | +| Project | `./.agents/skills/` | 跨客户端互操作 | +| User | `~/.deepcode/skills/` | Deep Code 原生位置 | +| User | `~/.agents/skills/` | 跨客户端互操作 | ### **为 DeepSeek 优化** - 专门为 DeepSeek 模型性能调优。 @@ -199,4 +205,4 @@ npm link [github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls [github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 [github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE -[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 \ No newline at end of file +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 diff --git a/src/cli.tsx b/src/cli.tsx index 56ebdb6f..6da6505d 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -27,9 +27,10 @@ if (args.includes("--help") || args.includes("-h")) { "Configuration:", " ~/.deepcode/settings.json User-level API key, model, base URL", " ./.deepcode/settings.json Project-level settings", - " ~/.agents/skills/*/SKILL.md User-level skills", - " ./.agents/skills/*/SKILL.md Project-level skills", - " ./.deepcode/skills/*/SKILL.md Legacy project-level skills", + " ./.deepcode/skills/*/SKILL.md Project-level native skills", + " ./.agents/skills/*/SKILL.md Project-level interoperable skills", + " ~/.deepcode/skills/*/SKILL.md User-level native skills", + " ~/.agents/skills/*/SKILL.md User-level interoperable skills", "", "Inside the TUI:", " enter Send the prompt", diff --git a/src/session.ts b/src/session.ts index 37a09c6f..cb32f661 100644 --- a/src/session.ts +++ b/src/session.ts @@ -787,9 +787,12 @@ The candidate skills are as follows:\n\n`; async listSkills(sessionId?: string): Promise { const homeDir = os.homedir(); - const agentsRoot = path.join(homeDir, ".agents", "skills"); - const legacyProjectSkillsRoot = path.join(this.projectRoot, ".deepcode", "skills"); - const projectAgentsSkillsRoot = path.join(this.projectRoot, ".agents", "skills"); + const skillRoots = [ + { root: path.join(this.projectRoot, ".deepcode", "skills"), displayRoot: "./.deepcode/skills" }, + { root: path.join(this.projectRoot, ".agents", "skills"), displayRoot: "./.agents/skills" }, + { root: path.join(homeDir, ".deepcode", "skills"), displayRoot: "~/.deepcode/skills" }, + { root: path.join(homeDir, ".agents", "skills"), displayRoot: "~/.agents/skills" }, + ]; const skillsByName = new Map(); const collectSkills = (root: string, displayRoot: string): SkillInfo[] => { @@ -826,14 +829,12 @@ The candidate skills are as follows:\n\n`; return results; }; - for (const skill of collectSkills(agentsRoot, "~/.agents/skills")) { - skillsByName.set(skill.name, skill); - } - for (const skill of collectSkills(legacyProjectSkillsRoot, "./.deepcode/skills")) { - skillsByName.set(skill.name, skill); - } - for (const skill of collectSkills(projectAgentsSkillsRoot, "./.agents/skills")) { - skillsByName.set(skill.name, skill); + for (const { root, displayRoot } of skillRoots) { + for (const skill of collectSkills(root, displayRoot)) { + if (!skillsByName.has(skill.name)) { + skillsByName.set(skill.name, skill); + } + } } if (sessionId) { diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index a818b23a..bf826c0a 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -415,7 +415,7 @@ test("SessionManager marks skills loaded from existing session messages", async assert.equal(loadedSkill?.isLoaded, true); }); -test("SessionManager lists project skills from .agents with legacy .deepcode compatibility", async () => { +test("SessionManager lists skills from Deep Code and .agents roots by priority", async () => { const workspace = createTempDir("deepcode-project-skills-workspace-"); const home = createTempDir("deepcode-project-skills-home-"); setHomeDir(home); @@ -428,11 +428,19 @@ test("SessionManager lists project skills from .agents with legacy .deepcode com "utf8" ); - const legacyProjectSkillDir = path.join(workspace, ".deepcode", "skills", "legacy"); - fs.mkdirSync(legacyProjectSkillDir, { recursive: true }); + const userNativeSkillDir = path.join(home, ".deepcode", "skills", "native-user"); + fs.mkdirSync(userNativeSkillDir, { recursive: true }); fs.writeFileSync( - path.join(legacyProjectSkillDir, "SKILL.md"), - "---\nname: legacy\ndescription: Legacy project skill\n---\n# Legacy\n", + path.join(userNativeSkillDir, "SKILL.md"), + "---\nname: native-user\ndescription: User .deepcode skill\n---\n# Native User\n", + "utf8" + ); + + const userNativeSharedSkillDir = path.join(home, ".deepcode", "skills", "shared"); + fs.mkdirSync(userNativeSharedSkillDir, { recursive: true }); + fs.writeFileSync( + path.join(userNativeSharedSkillDir, "SKILL.md"), + "---\nname: shared\ndescription: User .deepcode skill\n---\n# Shared\n", "utf8" ); @@ -444,15 +452,23 @@ test("SessionManager lists project skills from .agents with legacy .deepcode com "utf8" ); + const projectNativeSkillDir = path.join(workspace, ".deepcode", "skills", "shared"); + fs.mkdirSync(projectNativeSkillDir, { recursive: true }); + fs.writeFileSync( + path.join(projectNativeSkillDir, "SKILL.md"), + "---\nname: shared\ndescription: Project .deepcode skill\n---\n# Shared\n", + "utf8" + ); + const manager = createSessionManager(workspace, "machine-id-project-skills"); const skills = await manager.listSkills(); - const legacySkill = skills.find((skill) => skill.name === "legacy"); + const nativeUserSkill = skills.find((skill) => skill.name === "native-user"); const sharedSkill = skills.find((skill) => skill.name === "shared"); - assert.equal(legacySkill?.path, "./.deepcode/skills/legacy/SKILL.md"); - assert.equal(legacySkill?.description, "Legacy project skill"); - assert.equal(sharedSkill?.path, "./.agents/skills/shared/SKILL.md"); - assert.equal(sharedSkill?.description, "Project .agents skill"); + assert.equal(nativeUserSkill?.path, "~/.deepcode/skills/native-user/SKILL.md"); + assert.equal(nativeUserSkill?.description, "User .deepcode skill"); + assert.equal(sharedSkill?.path, "./.deepcode/skills/shared/SKILL.md"); + assert.equal(sharedSkill?.description, "Project .deepcode skill"); }); test("SessionManager dispose disconnects MCP servers", async () => { From 82c700a598916a1be1b3d3651c5f8d3fa964a1d3 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 5 Jun 2026 11:13:54 +0800 Subject: [PATCH 135/212] feat: include agent instructions in skill matching system prompt --- src/session.ts | 16 ++++++-- src/tests/session.test.ts | 82 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/session.ts b/src/session.ts index cb32f661..7ba57b25 100644 --- a/src/session.ts +++ b/src/session.ts @@ -718,7 +718,7 @@ export class SessionManager { options?: { signal?: AbortSignal; sessionId?: string } ): Promise { this.throwIfAborted(options?.signal); - let systemPrompt = `When users ask you to perform tasks, check if any of the available skills match. Skills provide specialized capabilities and domain knowledge.\n + let systemPrompt = `When users ask you to perform tasks, check if any of the available skills match the goal and situation. Skills provide specialized capabilities and domain knowledge.\n Response in JSON format: \`\`\` { @@ -726,7 +726,7 @@ Response in JSON format: } \`\`\`\n If none of the available skills match, respond with an empty array, i.e. \`{"skillNames": []}\`.\n -The candidate skills are as follows:\n\n`; +`; const simpleSkills = skills .filter((x) => !x.isLoaded) .map((x) => { @@ -735,13 +735,23 @@ The candidate skills are as follows:\n\n`; if (simpleSkills.length === 0) { return []; } - systemPrompt += "```\n" + JSON.stringify(simpleSkills, null, 2) + "\n```"; const { client, model, baseURL, debugLogEnabled } = this.createOpenAIClient(); if (!client) { return []; } + const agentInstructions = this.loadAgentInstructions(); + if (agentInstructions) { + systemPrompt += `Use the current agent instructions as additional context when deciding which skills match:\n + +${agentInstructions} +\n +`; + } + systemPrompt += "The candidate skills are as follows:\n\n"; + systemPrompt += "```\n" + JSON.stringify(simpleSkills, null, 2) + "\n```"; + try { const response = await this.createChatCompletionStream( client, diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index bf826c0a..b7660644 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -893,6 +893,88 @@ test("createSession appends default system prompts in prefix-cache-friendly orde assert.equal(systemContents[3], "root project instructions"); }); +test("createSession includes agent instructions in the skill matching system prompt", async () => { + const workspace = createTempDir("deepcode-skill-match-create-workspace-"); + const home = createTempDir("deepcode-skill-match-create-home-"); + setHomeDir(home); + globalThis.fetch = (async () => ({ ok: true, text: async () => "" }) as Response) as typeof fetch; + + fs.mkdirSync(path.join(workspace, ".deepcode"), { recursive: true }); + fs.writeFileSync(path.join(workspace, ".deepcode", "AGENTS.md"), "prefer project-specific skill matching", "utf8"); + const skillDir = path.join(workspace, ".deepcode", "skills", "project-aware"); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync( + path.join(skillDir, "SKILL.md"), + "---\nname: project-aware\ndescription: Match project-specific instructions\n---\n# Project Aware\n", + "utf8" + ); + + const requests: any[] = []; + const client = { + chat: { + completions: { + create: async (request: any) => { + requests.push(request); + return { choices: [{ message: { content: '{"skillNames":[]}' } }] }; + }, + }, + }, + }; + const manager = createMockedClientSessionManagerWithClient(workspace, client); + (manager as any).activateSession = async () => {}; + + await manager.createSession({ text: "pick the right workflow" }); + + const messages = (requests[0]?.messages ?? []) as Array<{ role?: string; content?: string }>; + assert.equal(messages[0]?.role, "system"); + assert.match(messages[0]?.content ?? "", //); + assert.match(messages[0]?.content ?? "", /prefer project-specific skill matching/); + assert.match(messages[0]?.content ?? "", /<\/agent-instructions>/); + assert.match(messages[0]?.content ?? "", /The candidate skills are as follows/); + assert.equal(messages[1]?.role, "user"); +}); + +test("replySession includes current agent instructions in the skill matching system prompt", async () => { + const workspace = createTempDir("deepcode-skill-match-reply-workspace-"); + const home = createTempDir("deepcode-skill-match-reply-home-"); + setHomeDir(home); + globalThis.fetch = (async () => ({ ok: true, text: async () => "" }) as Response) as typeof fetch; + + const requests: any[] = []; + const client = { + chat: { + completions: { + create: async (request: any) => { + requests.push(request); + return { choices: [{ message: { content: '{"skillNames":[]}' } }] }; + }, + }, + }, + }; + const manager = createMockedClientSessionManagerWithClient(workspace, client); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "" }); + fs.writeFileSync(path.join(workspace, "AGENTS.md"), "use reply-time agent instructions", "utf8"); + const skillDir = path.join(workspace, ".agents", "skills", "reply-aware"); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync( + path.join(skillDir, "SKILL.md"), + "---\nname: reply-aware\ndescription: Match reply-time instructions\n---\n# Reply Aware\n", + "utf8" + ); + + await manager.replySession(sessionId, { text: "pick the reply workflow" }); + + const messages = (requests[0]?.messages ?? []) as Array<{ role?: string; content?: string }>; + assert.equal(messages[0]?.role, "system"); + assert.match(messages[0]?.content ?? "", //); + assert.match(messages[0]?.content ?? "", /use reply-time agent instructions/); + assert.match(messages[0]?.content ?? "", /<\/agent-instructions>/); + assert.match(messages[0]?.content ?? "", /The candidate skills are as follows/); + assert.equal(messages[1]?.role, "user"); +}); + test("replySession stores /init and sends the active root project AGENTS path to the LLM", async () => { const workspace = createTempDir("deepcode-init-root-workspace-"); const home = createTempDir("deepcode-init-root-home-"); From bd35d8fc8ef890e620320b9e83290bb91babe28e Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 5 Jun 2026 11:30:36 +0800 Subject: [PATCH 136/212] refactor: SessionManager.appendDeferredPermissionPrompt --- src/session.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/session.ts b/src/session.ts index 7ba57b25..29c6699a 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1275,7 +1275,9 @@ ${skillMd} permissionOverrides: permissionPrompt?.permissions, messagePermissions: pendingToolCallMessage.message?.meta?.permissions, }); - permissionPrompt = await this.appendDeferredPermissionPrompt(sessionId, permissionPrompt, sessionController); + await this.appendDeferredPermissionPrompt(sessionId, permissionPrompt, sessionController); + // Permission replies are one-shot: do not reuse decisions or append the deferred user prompt again on later tool-call batches. + permissionPrompt = undefined; if (this.isInterrupted(sessionId)) { return; } @@ -2298,9 +2300,9 @@ ${skillMd} sessionId: string, userPrompt: UserPromptContent | undefined, controller: AbortController - ): Promise { + ): Promise { if (!userPrompt || this.isContinuePrompt(userPrompt)) { - return undefined; + return; } const text = userPrompt.text ?? ""; const hasUserContent = @@ -2308,7 +2310,7 @@ ${skillMd} (Array.isArray(userPrompt.imageUrls) && userPrompt.imageUrls.length > 0) || (Array.isArray(userPrompt.skills) && userPrompt.skills.length > 0); if (!hasUserContent) { - return undefined; + return; } this.reportNewPrompt(); const signal = controller.signal; @@ -2343,7 +2345,6 @@ ${skillMd} this.onAssistantMessage(skillMessage, true); } } - return undefined; } private buildToolParamsSnippet(toolFunction: unknown | null): string { From d23fccd8cfc2aaf9e2885a6a5b8001a5c3daefd1 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 5 Jun 2026 13:09:56 +0800 Subject: [PATCH 137/212] feat: add skill resource management and update skill document rendering --- src/prompt.ts | 119 +++++++++++++++++++++++++++++++++++++-- src/session.ts | 31 +++++----- src/tests/prompt.test.ts | 88 ++++++++++++++++++++++++++++- 3 files changed, 216 insertions(+), 22 deletions(-) diff --git a/src/prompt.ts b/src/prompt.ts index 7b98d457..c5c888c6 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -100,6 +100,30 @@ type PromptToolOptions = { }; const DEFAULT_SKILL_TEMPLATES = ["karpathy-guidelines.md"]; +const DEFAULT_SKILL_RESOURCE_FILE_LIMIT = 50; +const SKILL_RESOURCE_EXCLUDED_DIRS = new Set([ + ".cache", + ".git", + ".next", + ".turbo", + "build", + "coverage", + "dist", + "node_modules", + "out", +]); + +export type SkillPromptDocument = { + name: string; + content: string; + path?: string; + skillFilePath?: string; +}; + +type SkillResourceListing = { + files: string[]; + truncated: boolean; +}; function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): string { const toolsDir = path.join(extensionRoot, "templates", "tools"); @@ -149,14 +173,99 @@ export function getDefaultSkillPrompt(): string { return ""; } - const blocks = skillDocs.map( - (skill) => `<${skill.name}-skill> -${skill.content} -` - ); + return buildSkillDocumentsPrompt(skillDocs); +} + +export function buildSkillDocumentsPrompt(skills: SkillPromptDocument[]): string { + const blocks = skills.map((skill) => renderSkillDocumentBlock(skill)); return `Use the skill documents below to assist the user:\n${blocks.join("\n\n")}`; } +function renderSkillDocumentBlock(skill: SkillPromptDocument): string { + const pathAttribute = skill.path ? ` path="${escapeXml(skill.path)}"` : ""; + const resources = renderSkillResources(skill.skillFilePath); + return `<${skill.name}-skill${pathAttribute}> +${skill.content}${resources} +`; +} + +function renderSkillResources(skillFilePath?: string): string { + if (!skillFilePath) { + return ""; + } + + const listing = listSkillResourceFiles(skillFilePath, DEFAULT_SKILL_RESOURCE_FILE_LIMIT); + if (listing.files.length === 0 && !listing.truncated) { + return ""; + } + + const fileLines = listing.files.map((file) => ` ${escapeXml(file)}`); + const noteLine = listing.truncated + ? [` Listing capped at ${DEFAULT_SKILL_RESOURCE_FILE_LIMIT} files and may be incomplete.`] + : []; + return `\n\n\n${[...fileLines, ...noteLine].join("\n")}\n`; +} + +function listSkillResourceFiles(skillFilePath: string, limit: number): SkillResourceListing { + const skillDir = path.dirname(skillFilePath); + const files: string[] = []; + let truncated = false; + + const visit = (dir: string, relativeDir = ""): void => { + if (files.length > limit) { + truncated = true; + return; + } + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); + } catch { + return; + } + + for (const entry of entries) { + if (entry.name.startsWith(".")) { + continue; + } + + const relativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name; + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (SKILL_RESOURCE_EXCLUDED_DIRS.has(entry.name)) { + continue; + } + visit(fullPath, relativePath); + if (truncated) { + return; + } + continue; + } + + if (!entry.isFile() || entry.name === "SKILL.md") { + continue; + } + + files.push(toPosixPath(relativePath)); + if (files.length > limit) { + truncated = true; + return; + } + } + }; + + visit(skillDir); + return { files: files.slice(0, limit), truncated }; +} + +function toPosixPath(filePath: string): string { + return filePath.split(path.sep).join("/"); +} + +function escapeXml(value: string): string { + return value.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); +} + function getCurrentDateAndModelPrompt(model?: string): string { const date = new Date(); let prompt = `今天是${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日。随着对话的进行,时间在流逝。`; diff --git a/src/session.ts b/src/session.ts index 29c6699a..cf05de83 100644 --- a/src/session.ts +++ b/src/session.ts @@ -10,6 +10,7 @@ import { buildThinkingRequestOptions } from "./common/openai-thinking"; import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; import { readTextFileWithMetadata } from "./common/file-utils"; import { + buildSkillDocumentsPrompt, getCompactPrompt, getDefaultSkillPrompt, getExtensionRoot, @@ -878,6 +879,18 @@ ${agentInstructions} return path.join(os.homedir(), skillPath); } + private buildSkillPrompt(skill: SkillInfo): string { + const skillPath = this.resolveSkillPath(skill.path); + return buildSkillDocumentsPrompt([ + { + name: skill.name, + content: fs.readFileSync(skillPath, "utf8"), + path: skillPath, + skillFilePath: skillPath, + }, + ]); + } + private readSkillInfo(skillPath: string, displayPath: string, fallbackName: string): SkillInfo { const fallbackSkill: SkillInfo = { name: fallbackName.replace(/_/g, "-"), @@ -1101,11 +1114,7 @@ ${agentInstructions} if (skill.isLoaded) { continue; } - const skillMd = fs.readFileSync(this.resolveSkillPath(skill.path), "utf8"); - const skillPrompt = `Use the skill document below to assist the user:\n -<${skill.name}-skill path="${this.resolveSkillPath(skill.path)}"> -${skillMd} -`; + const skillPrompt = this.buildSkillPrompt(skill); const skillMessage = this.buildSkillMessage(sessionId, skillPrompt, skill); this.appendSessionMessage(sessionId, skillMessage); this.onAssistantMessage(skillMessage, true); @@ -1180,11 +1189,7 @@ ${skillMd} if (skill.isLoaded) { continue; } - const skillMd = fs.readFileSync(this.resolveSkillPath(skill.path), "utf8"); - const skillPrompt = `Use the skill document below to assist the user:\n -<${skill.name}-skill path="${this.resolveSkillPath(skill.path)}"> -${skillMd} -`; + const skillPrompt = this.buildSkillPrompt(skill); const skillMessage = this.buildSkillMessage(sessionId, skillPrompt, skill); this.appendSessionMessage(sessionId, skillMessage); this.onAssistantMessage(skillMessage, true); @@ -2335,11 +2340,7 @@ ${skillMd} if (skill.isLoaded) { continue; } - const skillMd = fs.readFileSync(this.resolveSkillPath(skill.path), "utf8"); - const skillPrompt = `Use the skill document below to assist the user:\n -<${skill.name}-skill path="${this.resolveSkillPath(skill.path)}"> -${skillMd} -`; + const skillPrompt = this.buildSkillPrompt(skill); const skillMessage = this.buildSkillMessage(sessionId, skillPrompt, skill); this.appendSessionMessage(sessionId, skillMessage); this.onAssistantMessage(skillMessage, true); diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts index 31f16a63..cef6c620 100644 --- a/src/tests/prompt.test.ts +++ b/src/tests/prompt.test.ts @@ -1,11 +1,31 @@ -import { test } from "node:test"; +import { afterEach, test } from "node:test"; import assert from "node:assert/strict"; import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; import { fileURLToPath } from "url"; -import { getDefaultSkillPrompt, getRuntimeContext, getSystemPrompt, getTools } from "../prompt"; +import { + buildSkillDocumentsPrompt, + getDefaultSkillPrompt, + getRuntimeContext, + getSystemPrompt, + getTools, +} from "../prompt"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const tempDirs: string[] = []; + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +function createTempDir(prefix: string): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} test("getTools always includes WebSearch", () => { const names = getTools().map((tool) => tool.function.name); @@ -68,6 +88,70 @@ test("getDefaultSkillPrompt loads the default skill template", () => { assert.equal(prompt.includes('path="templates/skills/'), false); }); +test("buildSkillDocumentsPrompt lists skill resources", () => { + const skillDir = createTempDir("deepcode-skill-resources-"); + fs.mkdirSync(path.join(skillDir, "scripts"), { recursive: true }); + fs.mkdirSync(path.join(skillDir, "references"), { recursive: true }); + const skillPath = path.join(skillDir, "SKILL.md"); + fs.writeFileSync(skillPath, "# PDF Skill\n", "utf8"); + fs.writeFileSync(path.join(skillDir, "scripts", "extract.py"), "print('extract')\n", "utf8"); + fs.writeFileSync(path.join(skillDir, "scripts", "merge.py"), "print('merge')\n", "utf8"); + fs.writeFileSync(path.join(skillDir, "references", "pdf-spec-summary.md"), "# PDF Spec\n", "utf8"); + + const prompt = buildSkillDocumentsPrompt([ + { name: "pdf", content: "# PDF Skill", path: skillPath, skillFilePath: skillPath }, + ]); + + assert.equal(prompt.includes(``), true); + assert.equal(prompt.includes(""), true); + assert.equal(prompt.includes("scripts/extract.py"), true); + assert.equal(prompt.includes("scripts/merge.py"), true); + assert.equal(prompt.includes("references/pdf-spec-summary.md"), true); + assert.equal(prompt.includes("SKILL.md"), false); +}); + +test("buildSkillDocumentsPrompt caps large skill resource listings", () => { + const skillDir = createTempDir("deepcode-skill-resource-cap-"); + const skillPath = path.join(skillDir, "SKILL.md"); + fs.writeFileSync(skillPath, "# Large Skill\n", "utf8"); + for (let index = 0; index < 55; index += 1) { + fs.writeFileSync(path.join(skillDir, `file-${String(index).padStart(2, "0")}.txt`), "resource\n", "utf8"); + } + + const prompt = buildSkillDocumentsPrompt([ + { name: "large", content: "# Large Skill", path: skillPath, skillFilePath: skillPath }, + ]); + + assert.equal((prompt.match(//g) ?? []).length, 50); + assert.equal(prompt.includes("file-49.txt"), true); + assert.equal(prompt.includes("file-50.txt"), false); + assert.equal(prompt.includes("Listing capped at 50 files and may be incomplete."), true); +}); + +test("buildSkillDocumentsPrompt excludes hidden and generated skill resources", () => { + const skillDir = createTempDir("deepcode-skill-resource-exclusions-"); + fs.mkdirSync(path.join(skillDir, ".hidden"), { recursive: true }); + fs.mkdirSync(path.join(skillDir, "node_modules", "pkg"), { recursive: true }); + fs.mkdirSync(path.join(skillDir, "dist"), { recursive: true }); + const skillPath = path.join(skillDir, "SKILL.md"); + fs.writeFileSync(skillPath, "# Clean Skill\n", "utf8"); + fs.writeFileSync(path.join(skillDir, ".secret.txt"), "hidden\n", "utf8"); + fs.writeFileSync(path.join(skillDir, ".hidden", "file.txt"), "hidden\n", "utf8"); + fs.writeFileSync(path.join(skillDir, "node_modules", "pkg", "index.js"), "module.exports = {}\n", "utf8"); + fs.writeFileSync(path.join(skillDir, "dist", "bundle.js"), "bundle\n", "utf8"); + fs.writeFileSync(path.join(skillDir, "README.md"), "# Resource\n", "utf8"); + + const prompt = buildSkillDocumentsPrompt([ + { name: "clean", content: "# Clean Skill", path: skillPath, skillFilePath: skillPath }, + ]); + + assert.equal(prompt.includes("README.md"), true); + assert.equal(prompt.includes(".secret.txt"), false); + assert.equal(prompt.includes(".hidden/file.txt"), false); + assert.equal(prompt.includes("node_modules/pkg/index.js"), false); + assert.equal(prompt.includes("dist/bundle.js"), false); +}); + test("getSystemPrompt does not include current date guidance", () => { const now = new Date(); const expected = `今天是${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日。随着对话的进行,时间在流逝。`; From 7799f527934076ffb5f97ef3e5e23785deb62681 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 5 Jun 2026 13:47:12 +0800 Subject: [PATCH 138/212] feat: add read permission exemption paths and apply to the scan paths for skills --- src/common/permissions.ts | 32 +++++++++++++++- src/session.ts | 9 ++++- src/tests/permissions.test.ts | 72 +++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/common/permissions.ts b/src/common/permissions.ts index 564bfeb8..822fcca4 100644 --- a/src/common/permissions.ts +++ b/src/common/permissions.ts @@ -60,6 +60,7 @@ export type ComputeToolCallPermissionsOptions = { projectRoot: string; toolCalls: unknown[]; settings?: Required; + readPermissionExemptPaths?: string[]; resolveSnippetPath?: (sessionId: string, snippetId: string) => string | null | undefined; }; @@ -159,6 +160,7 @@ export function computeToolCallPermissions(options: ComputeToolCallPermissionsOp sessionId: options.sessionId, projectRoot: options.projectRoot, toolCall, + readPermissionExemptPaths: options.readPermissionExemptPaths, resolveSnippetPath: options.resolveSnippetPath, }); const permission = evaluatePermissionScopes(request.scopes, options.settings); @@ -182,6 +184,7 @@ export function describeToolPermissionRequest(options: { sessionId: string; projectRoot: string; toolCall: PermissionToolCall; + readPermissionExemptPaths?: string[]; resolveSnippetPath?: (sessionId: string, snippetId: string) => string | null | undefined; }): AskPermissionRequest { const name = options.toolCall.function.name; @@ -193,7 +196,10 @@ export function describeToolPermissionRequest(options: { toolCallId: options.toolCall.id, name, command: formatToolPathCommand("read", filePath), - scopes: filePath ? [isPathInProject(options.projectRoot, filePath) ? "read-in-cwd" : "read-out-cwd"] : [], + scopes: + filePath && !isPathInAnyDirectory(options.projectRoot, filePath, options.readPermissionExemptPaths) + ? [isPathInProject(options.projectRoot, filePath) ? "read-in-cwd" : "read-out-cwd"] + : [], }; } @@ -386,6 +392,30 @@ export function isPathInProject(projectRoot: string, filePath: string): boolean return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } +export function isPathInAnyDirectory( + projectRoot: string, + filePath: string, + directories: string[] | undefined +): boolean { + if (!directories?.length) { + return false; + } + + const normalized = normalizeFilePath(filePath); + const absolutePath = isAbsoluteFilePath(normalized) ? normalized : path.resolve(projectRoot, normalized); + for (const directory of directories) { + const normalizedDirectory = normalizeFilePath(directory); + const absoluteDirectory = isAbsoluteFilePath(normalizedDirectory) + ? normalizedDirectory + : path.resolve(projectRoot, normalizedDirectory); + const relative = path.relative(path.resolve(absoluteDirectory), path.resolve(absolutePath)); + if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) { + return true; + } + } + return false; +} + export function hasUserPermissionReplies(value: { permissions?: unknown; alwaysAllows?: unknown }): boolean { return Boolean( (Array.isArray(value.permissions) && value.permissions.length > 0) || diff --git a/src/session.ts b/src/session.ts index cf05de83..9432a74d 100644 --- a/src/session.ts +++ b/src/session.ts @@ -796,14 +796,18 @@ ${agentInstructions} } } - async listSkills(sessionId?: string): Promise { + private getSkillScanRoots(): Array<{ root: string; displayRoot: string }> { const homeDir = os.homedir(); - const skillRoots = [ + return [ { root: path.join(this.projectRoot, ".deepcode", "skills"), displayRoot: "./.deepcode/skills" }, { root: path.join(this.projectRoot, ".agents", "skills"), displayRoot: "./.agents/skills" }, { root: path.join(homeDir, ".deepcode", "skills"), displayRoot: "~/.deepcode/skills" }, { root: path.join(homeDir, ".agents", "skills"), displayRoot: "~/.agents/skills" }, ]; + } + + async listSkills(sessionId?: string): Promise { + const skillRoots = this.getSkillScanRoots(); const skillsByName = new Map(); const collectSkills = (root: string, displayRoot: string): SkillInfo[] => { @@ -1354,6 +1358,7 @@ ${agentInstructions} projectRoot: this.projectRoot, toolCalls, settings: this.getResolvedSettings().permissions, + readPermissionExemptPaths: this.getSkillScanRoots().map((entry) => entry.root), resolveSnippetPath: (id, snippetId) => getSnippet(id, snippetId)?.filePath, }) : null; diff --git a/src/tests/permissions.test.ts b/src/tests/permissions.test.ts index 3a286167..5e8bf1e8 100644 --- a/src/tests/permissions.test.ts +++ b/src/tests/permissions.test.ts @@ -8,6 +8,7 @@ import { computeToolCallPermissions, evaluatePermissionScopes, hasUserPermissionReplies, + isPathInAnyDirectory, parseBashSideEffects, } from "../common/permissions"; @@ -126,6 +127,77 @@ test("computeToolCallPermissions only asks for scopes not already allowed", () = ); }); +test("computeToolCallPermissions allows read tool calls under skill scan paths", () => { + const projectRoot = createTempDir("deepcode-permissions-skill-read-workspace-"); + const home = createTempDir("deepcode-permissions-skill-read-home-"); + const skillRoot = path.join(home, ".agents", "skills"); + const skillResourcePath = path.join(skillRoot, "pdf", "scripts", "extract.py"); + const outsidePath = path.join(home, "notes.txt"); + const plan = computeToolCallPermissions({ + sessionId: "session-1", + projectRoot, + readPermissionExemptPaths: [skillRoot], + settings: { + allow: [], + deny: [], + ask: [], + defaultMode: "askAll", + }, + toolCalls: [ + { + id: "call-skill-read", + type: "function", + function: { name: "read", arguments: JSON.stringify({ file_path: skillResourcePath }) }, + }, + { + id: "call-outside-read", + type: "function", + function: { name: "read", arguments: JSON.stringify({ file_path: outsidePath }) }, + }, + ], + }); + + assert.deepEqual(plan.permissions, [ + { toolCallId: "call-skill-read", permission: "allow" }, + { toolCallId: "call-outside-read", permission: "ask" }, + ]); + assert.deepEqual( + plan.askPermissions.map((item) => ({ id: item.toolCallId, scopes: item.scopes })), + [{ id: "call-outside-read", scopes: ["read-out-cwd"] }] + ); +}); + +test("isPathInAnyDirectory matches absolute and project-relative directories without sibling leaks", () => { + const projectRoot = createTempDir("deepcode-permissions-directory-match-workspace-"); + const home = createTempDir("deepcode-permissions-directory-match-home-"); + const absoluteSkillRoot = path.join(home, ".agents", "skills"); + const relativeSkillRoot = path.join(".deepcode", "skills"); + + assert.equal( + isPathInAnyDirectory(projectRoot, path.join(absoluteSkillRoot, "pdf", "scripts", "extract.py"), [ + absoluteSkillRoot, + ]), + true + ); + assert.equal( + isPathInAnyDirectory(projectRoot, path.join(projectRoot, relativeSkillRoot, "local", "SKILL.md"), [ + relativeSkillRoot, + ]), + true + ); + assert.equal( + isPathInAnyDirectory(projectRoot, path.join(`${absoluteSkillRoot}-backup`, "extract.py"), [absoluteSkillRoot]), + false + ); + assert.equal( + isPathInAnyDirectory(projectRoot, path.join(projectRoot, ".deepcode", "skills-extra", "file.md"), [ + relativeSkillRoot, + ]), + false + ); + assert.equal(isPathInAnyDirectory(projectRoot, path.join(home, "notes.txt"), undefined), false); +}); + test("appendProjectPermissionAllows writes unique project-level allow scopes", () => { const projectRoot = createTempDir("deepcode-permission-settings-"); const settingsPath = path.join(projectRoot, ".deepcode", "settings.json"); From 67105127cd75f05ca78912579339e979e8dbc98c Mon Sep 17 00:00:00 2001 From: hcyang Date: Fri, 5 Jun 2026 15:52:12 +0800 Subject: [PATCH 139/212] =?UTF-8?q?feat(ui):=20=E4=BC=98=E5=8C=96=20Prompt?= =?UTF-8?q?Input=20=E7=BB=84=E4=BB=B6=E7=9A=84=E5=85=89=E6=A0=87=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=B8=8E=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将光标前缀包装在固定宽度的 Box 中,防止布局抖动 - 通过 getPromptCursorPlacement 计算光标位置,实现光标的精准定位 - 新增 usePromptTerminalCursor 钩子管理光标渲染 - 调整输入区宽度适配屏幕宽度,避免溢出 - 移除重复的终端焦点与光标隐藏钩子调用,优化副作用管理 - 统一控制终端光标显示,隐藏系统光标防止视觉冲突 --- src/ui/views/PromptInput.tsx | 39 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index 19342da5..48eb659a 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -43,7 +43,13 @@ import { } from "../core/file-mentions"; import type { FileMentionItem } from "../core/file-mentions"; import { readClipboardImageAsync } from "../core/clipboard"; -import { useTerminalInput, usePasteHandling, useHistoryNavigation } from "../hooks"; +import { + useTerminalInput, + usePasteHandling, + useHistoryNavigation, + getPromptCursorPlacement, + usePromptTerminalCursor, +} from "../hooks"; import type { InputKey } from "../hooks"; import { useHiddenTerminalCursor, @@ -108,7 +114,11 @@ const PromptPrefixLine = React.memo(function PromptPrefixLine({ busy }: { busy: }, [busy]); const prefix = busy ? `${SPINNER_FRAMES[spinnerIndex]} ` : "> "; - return {prefix}; + return ( + + {prefix} + + ); }); export const PromptInput = React.memo(function PromptInput({ @@ -202,9 +212,17 @@ export const PromptInput = React.memo(function PromptInput({ () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, [showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu] ); - // The prompt draws its own inverse-video cursor inside the text. Keep the - // native terminal cursor hidden so wrapping edges do not show two cursors. - const hideNativeCursor = !disabled; + + const cursorPlacement = useMemo( + () => getPromptCursorPlacement(buffer, screenWidth, 2, footerText), + [buffer, footerText, screenWidth] + ); + const usePositionedCursor = !disabled && hasTerminalFocus && !showFooterText; + useTerminalFocusReporting(stdout, !disabled); + useTerminalExtendedKeys(stdout, !disabled); + useBracketedPaste(stdout, !disabled); + usePromptTerminalCursor(stdout, cursorPlacement, usePositionedCursor); + useHiddenTerminalCursor(stdout, !disabled && !usePositionedCursor); const refreshFileMentionItems = React.useCallback(() => { setFileMentionItems(scanFileMentionItems(projectRoot)); @@ -560,10 +578,6 @@ export const PromptInput = React.memo(function PromptInput({ }, { isActive: !disabled } ); - useTerminalFocusReporting(stdout, !disabled); - useTerminalExtendedKeys(stdout, !disabled); - useBracketedPaste(stdout, !disabled); - useHiddenTerminalCursor(stdout, hideNativeCursor); function undo(): void { const previous = undoPromptEdit(undoRedoRef.current, buffer); @@ -742,6 +756,7 @@ export const PromptInput = React.memo(function PromptInput({ ) : null} {/* Input */} - {renderBufferWithCursor(buffer, !disabled && hasTerminalFocus, placeholder, pastesRef.current)} - {inlineHint ? {inlineHint} : null} + + {renderBufferWithCursor(buffer, !disabled && hasTerminalFocus, placeholder, pastesRef.current)} + {inlineHint ? {inlineHint} : null} + Date: Fri, 5 Jun 2026 18:03:51 +0800 Subject: [PATCH 140/212] 0.1.28 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7bd6dde..05d7c4eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.27", + "version": "0.1.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.27", + "version": "0.1.28", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index 23875a4d..0d7ef622 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.27", + "version": "0.1.28", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From b6232472a74627f0aa3f95d149cbe5fcdaa51c4f Mon Sep 17 00:00:00 2001 From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com> Date: Fri, 29 May 2026 16:37:41 +0800 Subject: [PATCH 141/212] fix(mcp): fix Windows MCP spawn double-quoting that breaks all MCP servers The quoteWindowsShellArg function wrapped every argument in double quotes unconditionally, producing command strings like '"npx" "-y" "@playwright/mcp"'. When passed to spawn() with shell:true, Node.js wraps the entire string again for cmd.exe, causing double-quoting that breaks command parsing. All MCP servers (playwright, fetch, memory, github) failed to start on Windows. Fix: only quote arguments that contain spaces or double-quotes, leaving simple args unquoted. --- src/mcp/mcp-client.ts | 15 ++++++++++----- src/tests/mcp-client.test.ts | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index 26a7a321..d2ef1c88 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -425,9 +425,11 @@ export function createMcpSpawnSpec( if (platform === "win32") { return { // On Windows, shell: true lets cmd.exe resolve the command via PATHEXT - // (npx -> npx.cmd, etc.). Pass one quoted command line with no spawn - // args to avoid Node 24 DEP0190. - command: [command, ...args].map(quoteWindowsShellArg).join(" "), + // (npx -> npx.cmd, etc.). Join command and args into a single string + // with empty spawn args to avoid Node 24 DEP0190. + // Only quote arguments that contain spaces or double-quotes to prevent + // double-wrapping by Node.js's own shell quoting. + command: [command, ...args].map(quoteWindowsArgIfNeeded).join(" "), args: [], shell: true, windowsHide: true, @@ -441,6 +443,9 @@ export function createMcpSpawnSpec( }; } -function quoteWindowsShellArg(arg: string): string { - return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, "$&$&")}"`; +function quoteWindowsArgIfNeeded(arg: string): string { + if (arg.includes(" ") || arg.includes('"')) { + return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, "$&$&")}"`; + } + return arg; } diff --git a/src/tests/mcp-client.test.ts b/src/tests/mcp-client.test.ts index e161aad3..29151d3a 100644 --- a/src/tests/mcp-client.test.ts +++ b/src/tests/mcp-client.test.ts @@ -10,9 +10,9 @@ test("createMcpSpawnSpec keeps non-Windows MCP launches shell-free", () => { }); }); -test("createMcpSpawnSpec avoids Windows shell args for Node 24", () => { +test("createMcpSpawnSpec joins args without quoting when spaces are absent (Windows)", () => { assert.deepEqual(createMcpSpawnSpec("npx", ["-y", "@playwright/mcp@latest"], "win32"), { - command: '"npx" "-y" "@playwright/mcp@latest"', + command: "npx -y @playwright/mcp@latest", args: [], shell: true, windowsHide: true, From 4b3339261548ed614a5f51c811a7e4626a328e80 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 8 Jun 2026 13:42:32 +0800 Subject: [PATCH 142/212] feat: implement `enabledSkills` support in settings.json --- docs/configuration.md | 18 +++++++++ docs/configuration_en.md | 18 +++++++++ src/session.ts | 9 ++++- src/settings.ts | 29 ++++++++++++++ src/tests/session.test.ts | 54 +++++++++++++++++++++++++++ src/tests/settings-and-notify.test.ts | 33 ++++++++++++++++ 6 files changed, 160 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 752f7ab7..2f198b10 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -36,6 +36,7 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 | `webSearchTool` | string | 自定义联网搜索脚本的完整路径 | | `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) | | `temperature` | number | 模型采样温度,范围 `0` 到 `2` | +| `enabledSkills` | object | 按 skill 名称启用或禁用 skill 的配置 | #### `env` 子字段 @@ -101,6 +102,23 @@ Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索 脚本接收一个搜索查询参数,输出 JSON 格式的结果供 AI 使用。 +#### `enabledSkills` — Skill 启用配置 + +控制 skill 扫描时是否包含指定 skill。键是解析后的 skill 名称,值必须是布尔值: + +```json +{ + "enabledSkills": { + "skill-writer": false, + "code-review": true + } +} +``` + +- 未配置的 skill 默认启用。 +- 将某个 skill 设置为 `false` 后,所有项目级和用户级目录中解析名称相同的 skill 都会被隐藏。 +- 项目设置会按 skill 覆盖用户设置。如果项目设置没有配置某个 skill,则使用用户设置。 + #### `mcpServers` — MCP 服务器 MCP(Model Context Protocol)服务器配置。值是键值对,键为服务名称,值为服务器配置对象。 diff --git a/docs/configuration_en.md b/docs/configuration_en.md index 8634992c..fac8c349 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -36,6 +36,7 @@ The following are all the top-level fields supported in `settings.json`, along w | `webSearchTool` | string | Full path to a custom web search script | | `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) | | `temperature` | number | Sampling temperature for LLM, from `0` to `2` | +| `enabledSkills` | object | Per-skill enable/disable map, keyed by skill name | #### `env` Sub-fields @@ -101,6 +102,23 @@ Deep Code has a built-in, free-to-use Web Search tool. If you need custom search The script receives a search query as an argument and outputs results in JSON format for the AI. +#### `enabledSkills` — Skill Enablement + +Controls whether skills are included during skill scanning. Keys are resolved skill names, and values must be booleans: + +```json +{ + "enabledSkills": { + "skill-writer": false, + "code-review": true + } +} +``` + +- Missing entries are enabled by default. +- Setting a skill to `false` hides every skill with that resolved `name`, across project and user skill roots. +- Project settings override user settings per skill. If the project setting omits a skill, the user setting is used. + #### `mcpServers` — MCP Servers Configuration for MCP (Model Context Protocol) servers. The value is a key-value pair, where the key is the service name and the value is a server configuration object. diff --git a/src/session.ts b/src/session.ts index 9432a74d..7c479c8c 100644 --- a/src/session.ts +++ b/src/session.ts @@ -298,6 +298,7 @@ type SessionManagerOptions = { webSearchTool?: string; mcpServers?: Record; permissions?: Required; + enabledSkills?: Record; }; renderMarkdown: (text: string) => string; onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; @@ -324,6 +325,7 @@ export class SessionManager { webSearchTool?: string; mcpServers?: Record; permissions?: Required; + enabledSkills?: Record; }; private readonly onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; private readonly onSessionEntryUpdated?: (entry: SessionEntry) => void; @@ -808,6 +810,7 @@ ${agentInstructions} async listSkills(sessionId?: string): Promise { const skillRoots = this.getSkillScanRoots(); + const enabledSkills = this.getResolvedSettings().enabledSkills ?? {}; const skillsByName = new Map(); const collectSkills = (root: string, displayRoot: string): SkillInfo[] => { @@ -839,7 +842,11 @@ ${agentInstructions} } catch { continue; } - results.push(this.readSkillInfo(skillPath, `${displayRoot}/${skillName}/SKILL.md`, skillName)); + const skill = this.readSkillInfo(skillPath, `${displayRoot}/${skillName}/SKILL.md`, skillName); + if (enabledSkills[skill.name] === false) { + continue; + } + results.push(skill); } return results; }; diff --git a/src/settings.ts b/src/settings.ts index deb90344..c91f0306 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -43,6 +43,8 @@ export type PermissionSettings = { defaultMode?: PermissionDefaultMode; }; +export type EnabledSkillsSettings = Record; + export type DeepcodingSettings = { env?: DeepcodingEnv; model?: string; @@ -55,6 +57,7 @@ export type DeepcodingSettings = { webSearchTool?: string; mcpServers?: Record; permissions?: PermissionSettings; + enabledSkills?: EnabledSkillsSettings; }; export type ResolvedDeepcodingSettings = { @@ -71,6 +74,7 @@ export type ResolvedDeepcodingSettings = { webSearchTool?: string; mcpServers?: Record; permissions: Required; + enabledSkills: EnabledSkillsSettings; }; export type ModelConfigSelection = { @@ -188,6 +192,30 @@ function mergePermissions( }; } +function normalizeEnabledSkills(value: unknown): EnabledSkillsSettings { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return {}; + } + const result: EnabledSkillsSettings = {}; + for (const [name, enabled] of Object.entries(value)) { + if (!name || typeof enabled !== "boolean") { + continue; + } + result[name] = enabled; + } + return result; +} + +function mergeEnabledSkills( + userSettings: DeepcodingSettings | null | undefined, + projectSettings: DeepcodingSettings | null | undefined +): EnabledSkillsSettings { + return { + ...normalizeEnabledSkills(userSettings?.enabledSkills), + ...normalizeEnabledSkills(projectSettings?.enabledSkills), + }; +} + function normalizeEnv(env: DeepcodingSettings["env"]): Record { const result: Record = {}; if (!env) { @@ -364,6 +392,7 @@ export function resolveSettingsSources( webSearchTool: webSearchTool || undefined, mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv), permissions: mergePermissions(userSettings, projectSettings), + enabledSkills: mergeEnabledSkills(userSettings, projectSettings), }; } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index b7660644..e22067fe 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -471,6 +471,60 @@ test("SessionManager lists skills from Deep Code and .agents roots by priority", assert.equal(sharedSkill?.description, "Project .deepcode skill"); }); +test("SessionManager excludes disabled skills by resolved skill name", async () => { + const workspace = createTempDir("deepcode-disabled-skills-workspace-"); + const home = createTempDir("deepcode-disabled-skills-home-"); + setHomeDir(home); + + const writeSkill = (root: string, dirName: string, skillName: string): void => { + const skillDir = path.join(root, dirName); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync( + path.join(skillDir, "SKILL.md"), + `---\nname: ${skillName}\ndescription: ${skillName} description\n---\n# ${skillName}\n`, + "utf8" + ); + }; + + for (const root of [ + path.join(workspace, ".deepcode", "skills"), + path.join(workspace, ".agents", "skills"), + path.join(home, ".deepcode", "skills"), + path.join(home, ".agents", "skills"), + ]) { + writeSkill(root, "skill-writer", "skill-writer"); + } + writeSkill(path.join(workspace, ".deepcode", "skills"), "frontmatter-disabled", "renamed-disabled"); + writeSkill(path.join(workspace, ".deepcode", "skills"), "enabled-skill", "enabled-skill"); + + const manager = new SessionManager({ + projectRoot: workspace, + createOpenAIClient: () => ({ + client: null, + model: "test-model", + baseURL: "https://api.deepseek.com", + thinkingEnabled: false, + machineId: "machine-id-disabled-skills", + }), + getResolvedSettings: () => ({ + model: "test-model", + enabledSkills: { + "skill-writer": false, + "renamed-disabled": false, + "enabled-skill": true, + }, + }), + renderMarkdown: (text) => text, + onAssistantMessage: () => {}, + }); + + const skills = await manager.listSkills(); + const skillNames = skills.map((skill) => skill.name); + + assert.deepEqual(skillNames, ["enabled-skill"]); + assert.equal(skills[0]?.path, "./.deepcode/skills/enabled-skill/SKILL.md"); +}); + test("SessionManager dispose disconnects MCP servers", async () => { const workspace = createTempDir("deepcode-mcp-dispose-workspace-"); const serverPath = path.join(workspace, "mcp-server.cjs"); diff --git a/src/tests/settings-and-notify.test.ts b/src/tests/settings-and-notify.test.ts index 5bc81cc9..ceddc43e 100644 --- a/src/tests/settings-and-notify.test.ts +++ b/src/tests/settings-and-notify.test.ts @@ -220,6 +220,39 @@ test("resolveSettingsSources merges permission settings", () => { assert.equal(resolved.permissions.defaultMode, "allowAll"); }); +test("resolveSettingsSources merges enabledSkills with project precedence", () => { + const resolved = resolveSettingsSources( + { + enabledSkills: { + inherited: false, + "project-enabled": false, + "project-disabled": true, + invalid: "false" as never, + }, + }, + { + enabledSkills: { + "project-enabled": true, + "project-disabled": false, + projectOnly: true, + ignored: null as never, + }, + }, + { + model: "default-model", + baseURL: "https://default.example.com", + }, + TEST_PROCESS_ENV + ); + + assert.deepEqual(resolved.enabledSkills, { + inherited: false, + "project-enabled": true, + "project-disabled": false, + projectOnly: true, + }); +}); + test("resolveSettingsSources merges MCP env with documented priority", () => { const resolved = resolveSettingsSources( { From 7fa00dcf4d1cb5be1daadba1103fb19f1808e9a0 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 8 Jun 2026 23:43:13 +0800 Subject: [PATCH 143/212] fix(ui): stabilize prompt cursor wrapping --- src/tests/message-view.test.ts | 23 +++ src/tests/prompt-input-keys.test.ts | 83 ++++++++-- src/ui/components/MessageView/index.tsx | 58 ++++--- src/ui/hooks/cursor.ts | 204 +++++++++++------------- src/ui/hooks/index.ts | 2 + src/ui/index.ts | 8 +- src/ui/views/PromptInput.tsx | 53 ++++-- 7 files changed, 274 insertions(+), 157 deletions(-) diff --git a/src/tests/message-view.test.ts b/src/tests/message-view.test.ts index ba1a9152..7d6b781c 100644 --- a/src/tests/message-view.test.ts +++ b/src/tests/message-view.test.ts @@ -1,6 +1,9 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import React from "react"; +import { renderToString } from "ink"; import { parseDiffPreview } from "../ui"; +import { MessageView, getPromptEchoContentWidth } from "../ui/components/MessageView"; import { buildThinkingSummary, formatBashStatusParams, @@ -119,6 +122,26 @@ test("renderMessageToStdout shows (no content) for empty user messages", () => { assert.ok(output.includes("(no content)")); }); +test("MessageView echoes submitted user prompts with live prompt wrapping width", () => { + assert.equal(getPromptEchoContentWidth(8), 6); + + const msg = makeSessionMessage({ role: "user", content: "abcdefg" }); + const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 }); + + assert.equal(output, "> abcdef\n g\n"); +}); + +test("MessageView echoes model changes with submitted prompt wrapping", () => { + const msg = makeSessionMessage({ + role: "system", + content: "abcdefgh", + meta: { isModelChange: true }, + }); + const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 }); + + assert.equal(output, "> abcdef\n gh\n"); +}); + test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => { const msg = makeSessionMessage({ role: "assistant", content: "Here is the fix" }); const output = renderMessageToStdout(msg, RawMode.Raw); diff --git a/src/tests/prompt-input-keys.test.ts b/src/tests/prompt-input-keys.test.ts index 4ca564f9..a8999b6b 100644 --- a/src/tests/prompt-input-keys.test.ts +++ b/src/tests/prompt-input-keys.test.ts @@ -13,8 +13,10 @@ import { formatSelectedSkillsStatus, getPromptCursorPlacement, getPromptReturnKeyAction, + isPromptCursorAtWrapBoundary, isClearImageAttachmentsShortcut, removeCurrentSlashToken, + resolvePromptTerminalCursorPosition, toggleSkillSelection, renderBufferWithCursor, buildInitPromptSubmission, @@ -294,24 +296,83 @@ test("renderBufferWithCursor styles exactly one simulated cursor", () => { assert.equal((renderBufferWithCursor({ text: "hello\nworld", cursor: 6 }, true).match(ANSI_RE) ?? []).length, 2); }); -test("getPromptCursorPlacement targets the prompt row above divider and footer", () => { - const placement = getPromptCursorPlacement({ text: "hello", cursor: 5 }, 80, 2, "Enter send"); - assert.deepEqual(placement, { rowsUp: 3, column: 7 }); +test("renderBufferWithCursor can suppress the simulated cursor for real terminal cursor mode", () => { + assert.equal( + (renderBufferWithCursor({ text: "", cursor: 0 }, true, undefined, undefined, false).match(ANSI_RE) ?? []).length, + 0 + ); + assert.equal( + stripAnsi(renderBufferWithCursor({ text: "", cursor: 0 }, true, "Ask anything", undefined, false)), + " Ask anything" + ); + assert.equal( + (renderBufferWithCursor({ text: "hello", cursor: 1 }, true, undefined, undefined, false).match(ANSI_RE) ?? []) + .length, + 0 + ); + assert.equal( + stripAnsi(renderBufferWithCursor({ text: "hello\n", cursor: 6 }, true, undefined, undefined, false)), + "hello\n " + ); +}); + +test("getPromptCursorPlacement targets an Ink-relative prompt cell", () => { + const placement = getPromptCursorPlacement({ text: "hello", cursor: 5 }, 80); + assert.deepEqual(placement, { row: 0, column: 5 }); }); test("getPromptCursorPlacement targets the reserved row after a trailing newline", () => { - const placement = getPromptCursorPlacement({ text: "hello\n", cursor: 6 }, 80, 2, "Enter send"); - assert.deepEqual(placement, { rowsUp: 3, column: 2 }); + const placement = getPromptCursorPlacement({ text: "hello\n", cursor: 6 }, 80); + assert.deepEqual(placement, { row: 1, column: 0 }); }); test("getPromptCursorPlacement accounts for CJK character width", () => { - const placement = getPromptCursorPlacement({ text: "你好", cursor: 2 }, 80, 2, "Enter send"); - assert.equal(placement.column, 6); + const placement = getPromptCursorPlacement({ text: "你好", cursor: 2 }, 80); + assert.equal(placement.column, 4); }); test("getPromptCursorPlacement accounts for multiline buffer rows", () => { - const placement = getPromptCursorPlacement({ text: "hello\nworld", cursor: 11 }, 80, 2, "Enter send"); - assert.deepEqual(placement, { rowsUp: 3, column: 7 }); - const middle = getPromptCursorPlacement({ text: "hello\nworld", cursor: 2 }, 80, 2, "Enter send"); - assert.deepEqual(middle, { rowsUp: 4, column: 4 }); + const placement = getPromptCursorPlacement({ text: "hello\nworld", cursor: 11 }, 80); + assert.deepEqual(placement, { row: 1, column: 5 }); + const middle = getPromptCursorPlacement({ text: "hello\nworld", cursor: 2 }, 80); + assert.deepEqual(middle, { row: 0, column: 2 }); +}); + +test("getPromptCursorPlacement accounts for wrapped input rows", () => { + const placement = getPromptCursorPlacement({ text: "hello", cursor: 5 }, 5); + assert.deepEqual(placement, { row: 1, column: 0 }); + const cursorBeforeWrappedChar = getPromptCursorPlacement({ text: "hello!", cursor: 5 }, 5); + assert.deepEqual(cursorBeforeWrappedChar, { row: 1, column: 0 }); + const secondLine = getPromptCursorPlacement({ text: "hello!", cursor: 6 }, 5); + assert.deepEqual(secondLine, { row: 1, column: 1 }); +}); + +test("isPromptCursorAtWrapBoundary detects hard-wrapped cursor positions", () => { + assert.equal(isPromptCursorAtWrapBoundary({ text: "hell", cursor: 4 }, 5), false); + assert.equal(isPromptCursorAtWrapBoundary({ text: "hello", cursor: 5 }, 5), true); + assert.equal(isPromptCursorAtWrapBoundary({ text: "hello!", cursor: 6 }, 5), true); + assert.equal(isPromptCursorAtWrapBoundary({ text: "hello world", cursor: 6 }, 5), true); + assert.equal(isPromptCursorAtWrapBoundary({ text: "hello\n", cursor: 6 }, 5), false); + assert.equal(isPromptCursorAtWrapBoundary({ text: "hello\nworld", cursor: 11 }, 5), true); +}); + +test("resolvePromptTerminalCursorPosition requires matching measured layout", () => { + const placement = { row: 1, column: 4 }; + const origin = { layoutKey: "skills:1", left: 2, top: 3 }; + + assert.deepEqual(resolvePromptTerminalCursorPosition(placement, true, "skills:1", origin), { x: 6, y: 4 }); + assert.equal(resolvePromptTerminalCursorPosition(placement, true, "skills:0", origin), undefined); + assert.equal(resolvePromptTerminalCursorPosition(placement, false, "skills:1", origin), undefined); + assert.equal(resolvePromptTerminalCursorPosition(placement, true, "skills:1", null), undefined); +}); + +test("resolvePromptTerminalCursorPosition clamps negative terminal cells", () => { + assert.deepEqual( + resolvePromptTerminalCursorPosition({ row: 0, column: 1 }, true, "current", { + layoutKey: "current", + left: -5, + top: -1, + }), + { x: 0, y: 0 } + ); }); diff --git a/src/ui/components/MessageView/index.tsx b/src/ui/components/MessageView/index.tsx index 9c315516..66df9625 100644 --- a/src/ui/components/MessageView/index.tsx +++ b/src/ui/components/MessageView/index.tsx @@ -12,6 +12,8 @@ import { import type { DiffPreviewLine, MessageViewProps } from "./types"; import { RawMode, useRawModeContext } from "../../contexts"; +const PROMPT_ECHO_PREFIX_WIDTH = 2; + export function MessageView({ message, collapsed, width = 80 }: MessageViewProps): React.ReactElement | null { const { mode } = useRawModeContext(); if (!message.visible) { @@ -21,17 +23,11 @@ export function MessageView({ message, collapsed, width = 80 }: MessageViewProps if (message.role === "user") { const text = message.content || "(no content)"; return ( - - - {`>`} - - - {text} - {Array.isArray(message.contentParams) && message.contentParams.length > 0 ? ( - {` 📎 ${message.contentParams.length} image attachment(s)`} - ) : null} - - + ); } @@ -109,16 +105,7 @@ export function MessageView({ message, collapsed, width = 80 }: MessageViewProps if (message.role === "system") { // Render model change messages in the same style as user commands. if (message.meta?.isModelChange) { - return ( - - - {`>`} - - - {message.content} - - - ); + return ; } if (message.meta?.skill) { @@ -143,6 +130,35 @@ export function MessageView({ message, collapsed, width = 80 }: MessageViewProps return null; } +export function getPromptEchoContentWidth(width: number): number { + return Math.max(1, width - PROMPT_ECHO_PREFIX_WIDTH); +} + +function PromptEchoLine({ + text, + width, + attachmentCount = 0, +}: { + text: string; + width: number; + attachmentCount?: number; +}): React.ReactElement { + const contentWidth = getPromptEchoContentWidth(width); + return ( + + + {"> "} + + + + {text} + + {attachmentCount > 0 ? {` 📎 ${attachmentCount} image attachment(s)`} : null} + + + ); +} + function StatusLine({ bulletColor, name, diff --git a/src/ui/hooks/cursor.ts b/src/ui/hooks/cursor.ts index 07cc5779..677fa810 100644 --- a/src/ui/hooks/cursor.ts +++ b/src/ui/hooks/cursor.ts @@ -1,28 +1,19 @@ -import { useLayoutEffect, useRef } from "react"; +import { useCursor, useBoxMetrics } from "ink"; +import { useLayoutEffect, useState } from "react"; +import type { RefObject } from "react"; +import type { DOMElement } from "ink"; import type { PromptBufferState } from "../core/prompt-buffer"; -type CursorPlacement = { - rowsUp: number; +export type CursorPlacement = { + row: number; column: number; }; -type WriteFn = ( - chunk: string | Uint8Array, - encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), - callback?: (error?: Error | null) => void -) => boolean; - -function cursorUp(rows: number): string { - return rows > 0 ? `\u001B[${rows}A` : ""; -} - -function cursorDown(rows: number): string { - return rows > 0 ? `\u001B[${rows}B` : ""; -} - -function cursorForward(columns: number): string { - return columns > 0 ? `\u001B[${columns}C` : ""; -} +export type PromptCursorOrigin = { + layoutKey: string; + left: number; + top: number; +}; function showCursor(): string { return "\u001B[?25h"; @@ -59,44 +50,42 @@ export function disableTerminalExtendedKeys(): string { export function getPromptCursorPlacement( state: PromptBufferState, screenWidth: number, - prefixWidth: number, - footerText: string + initialColumn = 0 ): CursorPlacement { const width = Math.max(1, screenWidth); const cursor = Math.max(0, Math.min(state.cursor, state.text.length)); const beforeCursor = state.text.slice(0, cursor); - const at = state.text[cursor]; - const displayText = - beforeCursor + - (typeof at === "undefined" || at === "\n" ? " " : at) + - (at === "\n" ? "\n" : "") + - (typeof at === "undefined" ? "" : state.text.slice(cursor + 1)); - - const cursorPosition = measureTextPosition(beforeCursor, width, prefixWidth); - const promptRows = measureTextRows(displayText, width, prefixWidth); - const footerRows = 1 + measureTextRows(footerText, width, 0); - - return { - rowsUp: promptRows - 1 - cursorPosition.row + footerRows + 1, - column: cursorPosition.column, - }; + const cursorPosition = measureTextPosition(beforeCursor, width, initialColumn); + return { row: cursorPosition.row, column: cursorPosition.column }; } -function measureTextRows(text: string, width: number, initialColumn: number): number { - return measureTextPosition(text, width, initialColumn).row + 1; +export function isPromptCursorAtWrapBoundary(state: PromptBufferState, screenWidth: number): boolean { + const width = Math.max(1, screenWidth); + const cursor = Math.max(0, Math.min(state.cursor, state.text.length)); + const currentLineStart = state.text.lastIndexOf("\n", Math.max(0, cursor - 1)) + 1; + const currentLineBeforeCursor = state.text.slice(currentLineStart, cursor); + return measureTextPosition(currentLineBeforeCursor, width, 0).row > 0; } function measureTextPosition(text: string, width: number, initialColumn: number): { row: number; column: number } { let row = 0; let column = Math.min(initialColumn, width - 1); + let pendingWrap = false; for (const char of Array.from(text)) { if (char === "\n") { row++; column = Math.min(initialColumn, width - 1); + pendingWrap = false; continue; } + if (pendingWrap) { + row++; + column = Math.min(initialColumn, width - 1); + pendingWrap = false; + } + const charColumns = textWidth(char); if (column + charColumns > width) { row++; @@ -104,11 +93,15 @@ function measureTextPosition(text: string, width: number, initialColumn: number) } column += charColumns; if (column >= width) { - row++; - column = Math.min(initialColumn, width - 1); + column = width; + pendingWrap = true; } } + if (pendingWrap) { + return { row: row + 1, column: Math.min(initialColumn, width - 1) }; + } + return { row, column }; } @@ -144,90 +137,79 @@ function characterWidth(char: string): number { } export function usePromptTerminalCursor( - stdout: NodeJS.WriteStream | undefined, + targetRef: RefObject, placement: CursorPlacement, - isActive: boolean -): void { - const directWriteRef = useRef<((data: string) => void) | null>(null); - const activePlacementRef = useRef(null); - const lastPlacementRef = useRef(null); - const unmountingRef = useRef(false); + isActive: boolean, + layoutKey = "default" +): boolean { + const { setCursorPosition } = useCursor(); + const metrics = useBoxMetrics(targetRef as RefObject); + const [origin, setOrigin] = useState(null); useLayoutEffect(() => { - if (!stdout?.isTTY) { + if (!isActive || !metrics.hasMeasured) { return; } - const stream = stdout as NodeJS.WriteStream & { write: WriteFn }; - const originalWrite = stream.write; - const directWrite = (data: string) => { - originalWrite.call(stdout, data); - }; - const restorePromptCursor = () => { - if (unmountingRef.current) { - return; + const absolutePosition = getAbsoluteElementPosition(targetRef.current); + setOrigin((previous) => { + if (!absolutePosition) { + return previous === null ? previous : null; } - const activePlacement = activePlacementRef.current; - if (!activePlacement) { - return; + + if ( + previous?.layoutKey === layoutKey && + previous.left === absolutePosition.left && + previous.top === absolutePosition.top + ) { + return previous; } - directWrite("\r" + cursorDown(activePlacement.rowsUp) + hideCursor()); - activePlacementRef.current = null; - // Schedule a deferred re-position in case the layout effect does not - // re-run (e.g. a dropdown closed without changing the buffer). - Promise.resolve().then(() => { - if (unmountingRef.current || activePlacementRef.current) { - return; - } - const latest = directWriteRef.current; - const p = lastPlacementRef.current; - if (latest && p) { - latest(showCursor() + cursorUp(p.rowsUp) + "\r" + cursorForward(p.column)); - activePlacementRef.current = p; - } - }); - }; - const patchedWrite: WriteFn = (...args) => { - restorePromptCursor(); - return originalWrite.apply(stdout, args); - }; - directWriteRef.current = directWrite; - stream.write = patchedWrite; + return { + layoutKey, + left: absolutePosition.left, + top: absolutePosition.top, + }; + }); + }, [isActive, layoutKey, metrics.hasMeasured, metrics.height, metrics.left, metrics.top, metrics.width, targetRef]); + + const cursorPosition = resolvePromptTerminalCursorPosition(placement, isActive, layoutKey, origin); + setCursorPosition(cursorPosition); + return cursorPosition !== undefined; +} - return () => { - restorePromptCursor(); - stream.write = originalWrite; - directWriteRef.current = null; - }; - }, [stdout]); +export function resolvePromptTerminalCursorPosition( + placement: CursorPlacement, + isActive: boolean, + layoutKey: string, + origin: PromptCursorOrigin | null +): { x: number; y: number } | undefined { + if (!isActive || origin?.layoutKey !== layoutKey) { + return undefined; + } - useLayoutEffect(() => { - if (!isActive || !stdout?.isTTY) { - return; - } + return { + x: Math.max(0, Math.round(origin.left + placement.column)), + y: Math.max(0, Math.round(origin.top + placement.row)), + }; +} - unmountingRef.current = false; - const directWrite = directWriteRef.current; - if (!directWrite) { - return; - } +function getAbsoluteElementPosition(element: DOMElement | null): { left: number; top: number } | null { + let current: DOMElement | undefined = element ?? undefined; + let left = 0; + let top = 0; - directWrite(showCursor() + cursorUp(placement.rowsUp) + "\r" + cursorForward(placement.column)); - activePlacementRef.current = placement; - lastPlacementRef.current = placement; + while (current) { + const layout = current.yogaNode?.getComputedLayout(); + if (!layout) { + return null; + } + left += layout.left; + top += layout.top; + current = current.parentNode; + } - return () => { - unmountingRef.current = true; - lastPlacementRef.current = null; - const activePlacement = activePlacementRef.current; - if (!activePlacement) { - return; - } - directWrite("\r" + cursorDown(activePlacement.rowsUp) + hideCursor()); - activePlacementRef.current = null; - }; - }, [isActive, placement, stdout]); + return { left, top }; } export function useHiddenTerminalCursor(stdout: NodeJS.WriteStream | undefined, isActive: boolean): void { diff --git a/src/ui/hooks/index.ts b/src/ui/hooks/index.ts index 86245b65..226a6e98 100644 --- a/src/ui/hooks/index.ts +++ b/src/ui/hooks/index.ts @@ -8,6 +8,8 @@ export { usePromptTerminalCursor, useTerminalFocusReporting, getPromptCursorPlacement, + isPromptCursorAtWrapBoundary, + resolvePromptTerminalCursorPosition, } from "./cursor"; export { usePasteHandling } from "./usePasteHandling"; diff --git a/src/ui/index.ts b/src/ui/index.ts index ae1109ad..2504bbd8 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -6,7 +6,13 @@ import { export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; export { buildPromptDraftFromSessionMessage } from "./utils"; -export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./hooks/cursor"; +export { + disableTerminalExtendedKeys, + enableTerminalExtendedKeys, + getPromptCursorPlacement, + isPromptCursorAtWrapBoundary, + resolvePromptTerminalCursorPosition, +} from "./hooks/cursor"; export { default as AppContainer } from "./views/AppContainer"; export { AskUserQuestionPrompt } from "./views/AskUserQuestionPrompt"; export { MessageView } from "./components"; diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index 48eb659a..f550afa9 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text, useApp, useStdout } from "ink"; +import type { DOMElement } from "ink"; import chalk from "chalk"; import { ARGS_SEPARATOR } from "../constants"; import { @@ -48,6 +49,7 @@ import { usePasteHandling, useHistoryNavigation, getPromptCursorPlacement, + isPromptCursorAtWrapBoundary, usePromptTerminalCursor, } from "../hooks"; import type { InputKey } from "../hooks"; @@ -98,6 +100,7 @@ type Props = { }; const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const PROMPT_PREFIX_WIDTH = 2; const PromptPrefixLine = React.memo(function PromptPrefixLine({ busy }: { busy: boolean }): React.ReactElement { const [spinnerIndex, setSpinnerIndex] = useState(0); @@ -141,6 +144,7 @@ export const PromptInput = React.memo(function PromptInput({ }: Props): React.ReactElement { const { exit } = useApp(); const { stdout } = useStdout(); + const inputTextRef = useRef(null); const [buffer, setBuffer] = useState(EMPTY_BUFFER); const [imageUrls, setImageUrls] = useState([]); const [selectedSkills, setSelectedSkills] = useState([]); @@ -212,17 +216,28 @@ export const PromptInput = React.memo(function PromptInput({ () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, [showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu] ); + const inputContentWidth = Math.max(1, screenWidth - PROMPT_PREFIX_WIDTH); const cursorPlacement = useMemo( - () => getPromptCursorPlacement(buffer, screenWidth, 2, footerText), - [buffer, footerText, screenWidth] + () => getPromptCursorPlacement(buffer, inputContentWidth), + [buffer, inputContentWidth] + ); + const useInlineCursor = isPromptCursorAtWrapBoundary(buffer, inputContentWidth); + const usePositionedCursor = !disabled && hasTerminalFocus && !showFooterText && stdout.isTTY && !useInlineCursor; + const promptCursorLayoutKey = useMemo( + () => [screenWidth, imageUrls.length, selectedSkills.map((skill) => skill.name).join("\u001F")].join("\u001E"), + [imageUrls.length, screenWidth, selectedSkills] ); - const usePositionedCursor = !disabled && hasTerminalFocus && !showFooterText; useTerminalFocusReporting(stdout, !disabled); useTerminalExtendedKeys(stdout, !disabled); useBracketedPaste(stdout, !disabled); - usePromptTerminalCursor(stdout, cursorPlacement, usePositionedCursor); - useHiddenTerminalCursor(stdout, !disabled && !usePositionedCursor); + const terminalCursorActive = usePromptTerminalCursor( + inputTextRef, + cursorPlacement, + usePositionedCursor, + promptCursorLayoutKey + ); + useHiddenTerminalCursor(stdout, !disabled && !terminalCursorActive); const refreshFileMentionItems = React.useCallback(() => { setFileMentionItems(scanFileMentionItems(projectRoot)); @@ -765,8 +780,16 @@ export const PromptInput = React.memo(function PromptInput({ borderDimColor > - - {renderBufferWithCursor(buffer, !disabled && hasTerminalFocus, placeholder, pastesRef.current)} + + + {renderBufferWithCursor( + buffer, + !disabled && hasTerminalFocus, + placeholder, + pastesRef.current, + !terminalCursorActive + )} + {inlineHint ? {inlineHint} : null} @@ -885,24 +908,28 @@ export function renderBufferWithCursor( state: PromptBufferState, isFocused: boolean, placeholder?: string, - validPastes?: Map + validPastes?: Map, + showSimulatedCursor = true ): string { const text = state.text || ""; const cursor = Math.max(0, Math.min(state.cursor, text.length)); const validIds = validPastes ?? new Map(); if (text.length === 0 && placeholder) { - if (!isFocused) { + if (!isFocused || !showSimulatedCursor) { return chalk.dim(` ${placeholder}`); } return renderCursorCell(" ") + chalk.dim(` ${placeholder}`); } if (text.length === 0) { - return isFocused ? renderCursorCell(" ") : ""; + if (!isFocused) { + return ""; + } + return showSimulatedCursor ? renderCursorCell(" ") : " "; } - if (!isFocused) { + if (!isFocused || !showSimulatedCursor) { return highlightPasteMarkersInText(text, validIds); } @@ -910,7 +937,7 @@ export function renderBufferWithCursor( } function highlightPasteMarkersInText(s: string, validIds: Map): string { - if (!s.includes("[paste #")) return s; + if (!s.includes("[paste #")) return s.endsWith("\n") ? `${s} ` : s; PASTE_MARKER_REGEX.lastIndex = 0; let result = ""; let pos = 0; From ffc7682c9cd00ab1e299d8a1e7e396926175b88c Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 9 Jun 2026 10:12:52 +0800 Subject: [PATCH 144/212] feat: add docs/session-persistence.md --- docs/session-persistence.md | 139 +++++++++++++++++++++++++++++++++ docs/session-persistence_en.md | 139 +++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 docs/session-persistence.md create mode 100644 docs/session-persistence_en.md diff --git a/docs/session-persistence.md b/docs/session-persistence.md new file mode 100644 index 00000000..835d2881 --- /dev/null +++ b/docs/session-persistence.md @@ -0,0 +1,139 @@ +# 会话持久化机制 + +Deep Code 会把每个项目的会话记录保存在本机用户目录中。会话历史用于 `/resume`、`/continue` 和 `/undo`,不依赖当前终端进程是否仍在运行。 + +## 存储位置 + +每个项目都有独立的存储目录: + +```text +~/.deepcode/projects// +``` + +`` 由项目根目录路径生成。普通路径会转换为安全的目录名;路径过长时,Deep Code 会保留项目名的一部分,并追加稳定哈希,以避免存储路径过长。 + +项目存储目录包含以下主要文件和目录: + +| 路径 | 说明 | +| ---- | ---- | +| `sessions-index.json` | 当前项目的会话索引,保存会话列表和每个会话的概要信息。 | +| `.jsonl` | 单个会话的消息记录。每一行是一条 JSON 格式的消息。 | +| `file-history/.git` | 用于代码快照的内部 Git 仓库,供 `/undo` 恢复文件内容。 | + +## 持久化内容 + +### 会话索引 + +`sessions-index.json` 保存最近的会话条目。每个条目包含: + +- 会话 ID、标题、创建时间和更新时间。 +- 会话状态,例如 `pending`、`processing`、`completed`、`failed`、`interrupted`、`ask_permission`、`waiting_for_user`。 +- 最近一次 assistant 回复、思考内容、拒绝原因和失败原因。 +- 最近一次工具调用信息、token 用量和活跃 token 数。 +- 当前会话中仍被跟踪的子进程信息。 + +会话标题默认来自首次用户输入的前 100 个字符。使用会话列表中的重命名功能会更新索引里的标题。 + +### 消息文件 + +每个会话有一个独立的 JSONL 消息文件,文件名是 `.jsonl`。消息按追加顺序写入,常见字段包括: + +| 字段 | 说明 | +| ---- | ---- | +| `id` | 消息 ID。 | +| `sessionId` | 所属会话 ID。 | +| `role` | 消息角色:`system`、`user`、`assistant` 或 `tool`。 | +| `content` | 文本内容。 | +| `contentParams` | 结构化内容,例如图片输入。 | +| `messageParams` | 模型消息参数,例如 tool call ID、tool calls、reasoning content。 | +| `visible` | 是否在界面中显示。 | +| `compacted` | 是否已经被长会话压缩替代。 | +| `checkpointHash` | 与 `/undo` 关联的代码快照哈希。 | +| `meta` | 工具展示、skill、权限、摘要等附加信息。 | + +读取消息文件时,Deep Code 会逐行解析 JSON;无法解析的行会被忽略,以便尽量保留其余可用历史。 + +### 代码快照 + +Deep Code 使用 `file-history/.git` 保存代码快照。这个仓库只作为内部文件历史使用,不是项目仓库本身。 + +- 新会话会初始化一条以会话 ID 命名的内部分支。 +- 每次用户输入前,会记录已跟踪文件的状态。 +- 工具修改文件前后,会按需记录相关文件的状态。 +- 用户消息上的 `checkpointHash` 用来把某次对话位置和对应的代码状态关联起来。 + +快照只覆盖 Deep Code 已跟踪到的文件;无关文件不会因为 `/undo` 被任意改写。 + +## 会话生命周期 + +### 创建会话 + +创建新会话时,Deep Code 会: + +1. 生成新的会话 ID。 +2. 初始化该会话的代码快照分支。 +3. 在 `sessions-index.json` 中添加会话条目。 +4. 写入系统提示、运行时上下文、项目指令和用户消息。 +5. 启动模型请求,并在 assistant 回复和工具执行过程中持续更新索引和消息文件。 + +项目级会话列表最多保留最近 50 条记录。超过上限时,较旧会话会从索引中移除,其消息文件和相关运行时资源也会被清理。 + +### 继续会话 + +`/resume` 会显示当前项目的历史会话列表,并选择一个会话继续。 + +`/continue` 会优先继续当前活动会话;如果没有可继续的活动会话,则进入历史会话选择流程。 + +继续会话时,Deep Code 会读取会话消息文件,过滤已压缩的旧消息,修复未完成的工具调用上下文,并把可用历史转换为模型请求消息。 + +### 长会话压缩 + +当会话上下文过长时,Deep Code 会触发压缩流程: + +- 选取较早的一段非系统消息生成摘要。 +- 将这段旧消息标记为 `compacted: true`。 +- 在消息序列中插入一条不可见的系统摘要消息。 + +后续请求只会使用未压缩消息和摘要消息。原始消息仍保留在 JSONL 文件中,用于审计和界面历史展示。 + +### 中断、失败和权限等待 + +会话状态会随运行过程更新: + +- 用户中断后,状态会变为 `interrupted`,并清理当前会话控制器和被跟踪的子进程。 +- 请求失败时,状态会变为 `failed`,失败原因写入索引。 +- 工具调用需要确认时,状态会变为 `ask_permission`。 +- 工具需要用户输入时,状态会变为 `waiting_for_user`。 + +这些状态都会持久化到 `sessions-index.json`,因此重新打开 CLI 后仍能在会话列表中看到。 + +## `/undo` 如何使用持久化数据 + +`/undo` 的候选项来自可见且未压缩的用户消息。每个候选项会检查是否有关联的 `checkpointHash`,并确认对应快照是否可恢复。 + +根据选择,Deep Code 可以执行以下操作: + +| 操作 | 行为 | +| ---- | ---- | +| 恢复对话 | 截断所选用户消息之前的消息历史,并更新索引中的最新 assistant 信息。 | +| 恢复代码 | 从 `file-history/.git` 中读取所选快照,并还原被跟踪文件。 | +| 同时恢复 | 先恢复代码,再截断对话历史。 | + +恢复对话会重写该会话的 JSONL 文件;恢复代码会修改工作区中被快照跟踪的文件。 + +## 删除和重命名 + +在会话列表中删除会话会: + +- 从 `sessions-index.json` 移除该条目。 +- 删除对应的 `.jsonl` 文件。 +- 清理该会话的内存状态、临时工作目录状态、控制器和仍被跟踪的进程控制信息。 + +重命名会话只更新索引中的 `summary` 字段,不会改动消息文件或代码快照。 + +## 注意事项 + +- 会话数据保存在本机用户目录下,并按项目分隔。 +- 移动项目目录后,新的项目根路径会生成新的 ``;旧路径对应的历史不会自动迁移。 +- `file-history/.git` 是 Deep Code 的内部快照仓库,不应手动修改。 +- 会话删除不会清理内部 Git 仓库中的所有历史对象;它主要删除会话索引、消息文件和运行时资源。 diff --git a/docs/session-persistence_en.md b/docs/session-persistence_en.md new file mode 100644 index 00000000..071a5353 --- /dev/null +++ b/docs/session-persistence_en.md @@ -0,0 +1,139 @@ +# Session Persistence + +Deep Code stores per-project session history in the local user directory. This history powers `/resume`, `/continue`, and `/undo`, and it remains available after the current terminal process exits. + +## Storage Location + +Each project has its own storage directory: + +```text +~/.deepcode/projects// +``` + +`` is generated from the project root path. Normal paths are converted into safe directory names. When the path would be too long, Deep Code keeps part of the project name and appends a stable hash so the storage path stays safe. + +The project storage directory contains these main files and directories: + +| Path | Description | +| ---- | ----------- | +| `sessions-index.json` | Session index for the current project, including the session list and summary metadata. | +| `.jsonl` | Message log for one session. Each line is one JSON message. | +| `file-history/.git` | Internal Git repository used for code checkpoints restored by `/undo`. | + +## Persisted Data + +### Session Index + +`sessions-index.json` stores recent session entries. Each entry includes: + +- Session ID, title, creation time, and update time. +- Session status, such as `pending`, `processing`, `completed`, `failed`, `interrupted`, `ask_permission`, or `waiting_for_user`. +- Latest assistant reply, thinking content, refusal reason, and failure reason. +- Latest tool-call data, token usage, and active token count. +- Metadata for subprocesses still tracked by the session. + +The default session title comes from the first 100 characters of the first user prompt. Renaming a session from the session list updates the title in the index. + +### Message Files + +Each session has a separate JSONL message file named `.jsonl`. Messages are appended in order. Common fields include: + +| Field | Description | +| ----- | ----------- | +| `id` | Message ID. | +| `sessionId` | Owning session ID. | +| `role` | Message role: `system`, `user`, `assistant`, or `tool`. | +| `content` | Text content. | +| `contentParams` | Structured content, such as image input. | +| `messageParams` | Model message parameters, such as tool call IDs, tool calls, and reasoning content. | +| `visible` | Whether the message is shown in the UI. | +| `compacted` | Whether the message has been replaced by long-session compaction. | +| `checkpointHash` | Code checkpoint hash associated with `/undo`. | +| `meta` | Extra metadata for tool display, skills, permissions, summaries, and related features. | + +When loading a message file, Deep Code parses JSON one line at a time. Malformed lines are ignored so the remaining usable history can still be loaded. + +### Code Checkpoints + +Deep Code stores code checkpoints in `file-history/.git`. This repository is only internal file history; it is not the project Git repository. + +- A new session initializes an internal branch named after the session ID. +- Before each user prompt, Deep Code records the state of files it already tracks. +- Before and after tool-based file mutations, Deep Code records the relevant file state as needed. +- `checkpointHash` on user messages links a conversation position to a code state. + +Checkpoints only cover files Deep Code has tracked. Unrelated files are not arbitrarily rewritten by `/undo`. + +## Session Lifecycle + +### Creating A Session + +When creating a new session, Deep Code: + +1. Generates a new session ID. +2. Initializes the code checkpoint branch for that session. +3. Adds an entry to `sessions-index.json`. +4. Writes system prompts, runtime context, project instructions, and the user message. +5. Starts the model request and keeps updating the index and message file as assistant replies and tool executions complete. + +The per-project session list keeps the 50 most recent entries. When the limit is exceeded, older sessions are removed from the index, and their message files and related runtime resources are cleaned up. + +### Continuing A Session + +`/resume` shows the current project's session history and lets you select a session to continue. + +`/continue` first continues the active session. If there is no active session to continue, it opens the session selection flow. + +When continuing a session, Deep Code reads the message file, filters compacted old messages, repairs incomplete tool-call context, and converts the usable history into model request messages. + +### Long-Session Compaction + +When the conversation context grows too large, Deep Code can compact earlier messages: + +- It summarizes an older range of non-system messages. +- It marks those old messages as `compacted: true`. +- It inserts an invisible system summary message into the message sequence. + +Future requests use the remaining active messages and the summary message. The original messages stay in the JSONL file for auditability and UI history. + +### Interruptions, Failures, And Permission Waits + +Session status changes during execution: + +- After a user interruption, status becomes `interrupted`, and Deep Code clears the current session controller and tracked subprocesses. +- After a request failure, status becomes `failed`, and the failure reason is written to the index. +- When a tool call needs confirmation, status becomes `ask_permission`. +- When a tool needs user input, status becomes `waiting_for_user`. + +These states are persisted in `sessions-index.json`, so they remain visible in the session list after reopening the CLI. + +## How `/undo` Uses Persistent Data + +`/undo` candidates come from visible, non-compacted user messages. Each candidate is checked for an associated `checkpointHash`, and Deep Code verifies whether the checkpoint can be restored. + +Depending on the selected mode, Deep Code can perform these operations: + +| Operation | Behavior | +| --------- | -------- | +| Restore conversation | Truncates message history before the selected user message and updates the latest assistant data in the index. | +| Restore code | Reads the selected checkpoint from `file-history/.git` and restores tracked files. | +| Restore both | Restores code first, then truncates the conversation history. | + +Restoring conversation rewrites the session JSONL file. Restoring code modifies workspace files tracked by the selected checkpoint. + +## Delete And Rename + +Deleting a session from the session list: + +- Removes the entry from `sessions-index.json`. +- Deletes the matching `.jsonl` file. +- Clears in-memory state, temporary working-directory state, controllers, and tracked process controls for that session. + +Renaming a session only updates the `summary` field in the index. It does not change message files or code checkpoints. + +## Notes + +- Session data is stored in the local user directory and separated by project. +- If a project directory is moved, the new project root path generates a new ``; history for the old path is not migrated automatically. +- `file-history/.git` is Deep Code's internal checkpoint repository and should not be edited manually. +- Deleting a session does not remove every historical object from the internal Git repository. It mainly removes the session index entry, message file, and runtime resources. From 4cf5cc0bd86670ded58c9a8c4a737060266471cd Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 9 Jun 2026 11:28:11 +0800 Subject: [PATCH 145/212] fix(ui): improve prompt cursor wrapping and status line display --- src/ui/views/App.tsx | 71 +++++++++++++++++++++++++++++++++--- src/ui/views/PromptInput.tsx | 49 +++++++++++-------------- 2 files changed, 88 insertions(+), 32 deletions(-) diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index 1579848f..bc12962a 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -48,12 +48,47 @@ import { SessionManager } from "../../session"; type View = "chat" | "session-list" | "undo" | "mcp-status"; +const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + type AppProps = { projectRoot: string; initialPrompt?: string; onRestart?: () => void; }; +const StatusLine = React.memo(function StatusLine({ + busy, + text, +}: { + busy: boolean; + text?: string; +}): React.ReactElement { + const [spinnerIndex, setSpinnerIndex] = useState(0); + + useEffect(() => { + if (!busy) { + setSpinnerIndex(0); + return; + } + + const timer = setInterval(() => { + setSpinnerIndex((index) => (index + 1) % STATUS_SPINNER_FRAMES.length); + }, 80); + return () => clearInterval(timer); + }, [busy]); + + return ( + + {busy ? ( + + {STATUS_SPINNER_FRAMES[spinnerIndex]} + + ) : null} + {text ? {text} : null} + + ); +}); + function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement { const { exit } = useApp(); const { stdout, write } = useStdout(); @@ -641,6 +676,35 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl } return messages; }, [mode, showWelcome, view, messages, welcomeItem]); + const promptCursorLayoutKey = useMemo(() => { + const lastStaticItem = staticItems.at(-1); + return [ + view, + busy ? "busy" : "idle", + statusLine, + errorLine ?? "", + showProcessStdout ? "stdout" : "main", + activeStatus ?? "", + staticItems.length, + lastStaticItem?.id ?? "", + lastStaticItem?.updateTime ?? "", + shouldShowQuestionPrompt ? (pendingQuestion?.messageId ?? "") : "", + activeAskPermissions?.length ?? 0, + pendingPermissionReply ? "pending-permission-reply" : "no-pending-permission-reply", + ].join("\u001E"); + }, [ + activeAskPermissions, + activeStatus, + busy, + errorLine, + pendingPermissionReply, + pendingQuestion, + shouldShowQuestionPrompt, + showProcessStdout, + staticItems, + statusLine, + view, + ]); const handleQuestionAnswers = useCallback( (answers: AskUserQuestionAnswers) => { @@ -724,11 +788,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl ); }}
- {statusLine ? ( - - {statusLine} - - ) : null} + {busy || statusLine ? : null} {errorLine ? ( Error: {errorLine} @@ -802,6 +862,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl modelConfig={resolvedSettings} promptHistory={promptHistory} busy={busy} + cursorLayoutKey={promptCursorLayoutKey} loadingText={loadingText} runningProcesses={runningProcesses} promptDraft={promptDraft} diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index f550afa9..c81d2237 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -87,6 +87,7 @@ type Props = { screenWidth: number; promptHistory: string[]; busy: boolean; + cursorLayoutKey?: string; loadingText?: string | null; disabled?: boolean; placeholder?: string; @@ -99,27 +100,12 @@ type Props = { onToggleProcessStdout?: () => void; }; -const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const PROMPT_PREFIX_WIDTH = 2; -const PromptPrefixLine = React.memo(function PromptPrefixLine({ busy }: { busy: boolean }): React.ReactElement { - const [spinnerIndex, setSpinnerIndex] = useState(0); - - useEffect(() => { - if (!busy) { - setSpinnerIndex(0); - return; - } - const timer = setInterval(() => { - setSpinnerIndex((index) => (index + 1) % SPINNER_FRAMES.length); - }, 80); - return () => clearInterval(timer); - }, [busy]); - - const prefix = busy ? `${SPINNER_FRAMES[spinnerIndex]} ` : "> "; +const PromptPrefixLine = React.memo(function PromptPrefixLine(): React.ReactElement { return ( - - {prefix} + + {"> "} ); }); @@ -131,6 +117,7 @@ export const PromptInput = React.memo(function PromptInput({ screenWidth, promptHistory, busy, + cursorLayoutKey, loadingText, disabled, placeholder, @@ -205,12 +192,14 @@ export const PromptInput = React.memo(function PromptInput({ : hasExpandedRegions ? " · ctrl+o collapse" : ""; + const busyStatusText = + loadingText && loadingText.trim() + ? `${loadingText}${processOrPasteHint}` + : `esc to interrupt · ctrl+c to cancel input${processOrPasteHint}`; const footerText = statusMessage ? statusMessage : busy - ? loadingText && loadingText.trim() - ? `${loadingText}${processOrPasteHint}` - : `esc to interrupt · ctrl+c to cancel input${processOrPasteHint}` + ? busyStatusText : `enter send · shift+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${processOrPasteHint}`; const showFooterText = useMemo( () => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu, @@ -225,8 +214,14 @@ export const PromptInput = React.memo(function PromptInput({ const useInlineCursor = isPromptCursorAtWrapBoundary(buffer, inputContentWidth); const usePositionedCursor = !disabled && hasTerminalFocus && !showFooterText && stdout.isTTY && !useInlineCursor; const promptCursorLayoutKey = useMemo( - () => [screenWidth, imageUrls.length, selectedSkills.map((skill) => skill.name).join("\u001F")].join("\u001E"), - [imageUrls.length, screenWidth, selectedSkills] + () => + [ + screenWidth, + cursorLayoutKey ?? "default", + imageUrls.length, + selectedSkills.map((skill) => skill.name).join("\u001F"), + ].join("\u001E"), + [cursorLayoutKey, imageUrls.length, screenWidth, selectedSkills] ); useTerminalFocusReporting(stdout, !disabled); useTerminalExtendedKeys(stdout, !disabled); @@ -234,10 +229,10 @@ export const PromptInput = React.memo(function PromptInput({ const terminalCursorActive = usePromptTerminalCursor( inputTextRef, cursorPlacement, - usePositionedCursor, + !busy && usePositionedCursor, promptCursorLayoutKey ); - useHiddenTerminalCursor(stdout, !disabled && !terminalCursorActive); + useHiddenTerminalCursor(stdout, !disabled && (busy || !terminalCursorActive)); const refreshFileMentionItems = React.useCallback(() => { setFileMentionItems(scanFileMentionItems(projectRoot)); @@ -779,7 +774,7 @@ export const PromptInput = React.memo(function PromptInput({ borderRight={false} borderDimColor > - + {renderBufferWithCursor( @@ -787,7 +782,7 @@ export const PromptInput = React.memo(function PromptInput({ !disabled && hasTerminalFocus, placeholder, pastesRef.current, - !terminalCursorActive + !busy && !terminalCursorActive )} {inlineHint ? {inlineHint} : null} From 726034fb1f32653a10d9713cf8c4083b33403ef6 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 9 Jun 2026 13:34:58 +0800 Subject: [PATCH 146/212] feat: implement bundled built-in skills --- package.json | 3 +- scripts/copy_bundle_assets.js | 27 ++ src/session.ts | 27 +- src/tests/session.test.ts | 81 +++- .../bundled/deepcode-self-refer/SKILL.md | 125 ++++++ .../deepcode-self-refer/references/README.md | 209 ++++++++++ .../references/configuration.md | 217 ++++++++++ .../references/configuration_en.md | 216 ++++++++++ .../deepcode-self-refer/references/mcp.md | 200 +++++++++ .../deepcode-self-refer/references/mcp_en.md | 200 +++++++++ .../deepcode-self-refer/references/notify.md | 211 ++++++++++ .../references/notify_en.md | 211 ++++++++++ .../references/permission.md | 101 +++++ .../references/permission_en.md | 100 +++++ .../references/session-persistence.md | 139 +++++++ .../references/session-persistence_en.md | 139 +++++++ .../skills/bundled/skill-digester/SKILL.md | 115 ++++++ .../skill-digester/scripts/find-skill.js | 215 ++++++++++ .../skills/bundled/skill-writer/SKILL.md | 381 ++++++++++++++++++ 19 files changed, 2911 insertions(+), 6 deletions(-) create mode 100644 scripts/copy_bundle_assets.js create mode 100644 templates/skills/bundled/deepcode-self-refer/SKILL.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/README.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/configuration.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/configuration_en.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/mcp.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/mcp_en.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/notify.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/notify_en.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/permission.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/permission_en.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/session-persistence.md create mode 100644 templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md create mode 100644 templates/skills/bundled/skill-digester/SKILL.md create mode 100755 templates/skills/bundled/skill-digester/scripts/find-skill.js create mode 100644 templates/skills/bundled/skill-writer/SKILL.md diff --git a/package.json b/package.json index 0d7ef622..911bd74e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "main": "./dist/cli.js", "files": [ "dist/cli.js", + "dist/bundled/**", "templates/tools/**", "templates/prompts/**", "templates/skills/**", @@ -26,7 +27,7 @@ }, "scripts": { "typecheck": "tsc -p ./ --noEmit", - "bundle": "esbuild ./src/cli.tsx --bundle --platform=node --format=esm --target=node18 --outfile=dist/cli.js --banner:js=\"#!/usr/bin/env node\" --jsx=automatic --jsx-import-source=react --packages=external --log-override:empty-import-meta=silent", + "bundle": "esbuild ./src/cli.tsx --bundle --platform=node --format=esm --target=node18 --outfile=dist/cli.js --banner:js=\"#!/usr/bin/env node\" --jsx=automatic --jsx-import-source=react --packages=external --log-override:empty-import-meta=silent && node scripts/copy_bundle_assets.js", "lint": "eslint src/", "lint:fix": "eslint src/ --fix", "format": "prettier --write 'src/**/*.{ts,tsx}'", diff --git a/scripts/copy_bundle_assets.js b/scripts/copy_bundle_assets.js new file mode 100644 index 00000000..0e1dd948 --- /dev/null +++ b/scripts/copy_bundle_assets.js @@ -0,0 +1,27 @@ +import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/* global console, process */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); +const distDir = join(root, "dist"); +const bundledSkillsSrc = join(root, "templates", "skills", "bundled"); +const bundledSkillsDest = join(distDir, "bundled"); + +if (!existsSync(distDir)) { + mkdirSync(distDir, { recursive: true }); +} + +if (!existsSync(bundledSkillsSrc)) { + console.warn(`Bundled skills directory not found at ${bundledSkillsSrc}`); + process.exit(0); +} + +rmSync(bundledSkillsDest, { recursive: true, force: true }); +cpSync(bundledSkillsSrc, bundledSkillsDest, { + recursive: true, + dereference: true, +}); +console.log("Copied bundled built-in skills to dist/bundled/"); diff --git a/src/session.ts b/src/session.ts index 7c479c8c..5e00b49e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -805,9 +805,22 @@ ${agentInstructions} { root: path.join(this.projectRoot, ".agents", "skills"), displayRoot: "./.agents/skills" }, { root: path.join(homeDir, ".deepcode", "skills"), displayRoot: "~/.deepcode/skills" }, { root: path.join(homeDir, ".agents", "skills"), displayRoot: "~/.agents/skills" }, + { root: this.getBundledSkillsRoot(), displayRoot: "bundled:" }, ]; } + private getBundledSkillsRoot(): string { + const extensionRoot = getExtensionRoot(); + const sourceRoot = path.join(extensionRoot, "templates", "skills", "bundled"); + const distRoot = path.join(extensionRoot, "dist", "bundled"); + + // Source check keeps local development/tests on the checked-in templates. + if (fs.existsSync(path.join(extensionRoot, "src", "session.ts")) && fs.existsSync(sourceRoot)) { + return sourceRoot; + } + return fs.existsSync(distRoot) ? distRoot : sourceRoot; + } + async listSkills(sessionId?: string): Promise { const skillRoots = this.getSkillScanRoots(); const enabledSkills = this.getResolvedSettings().enabledSkills ?? {}; @@ -842,7 +855,9 @@ ${agentInstructions} } catch { continue; } - const skill = this.readSkillInfo(skillPath, `${displayRoot}/${skillName}/SKILL.md`, skillName); + const displayPath = + displayRoot === "bundled:" ? `bundled:${skillName}/SKILL.md` : `${displayRoot}/${skillName}/SKILL.md`; + const skill = this.readSkillInfo(skillPath, displayPath, skillName); if (enabledSkills[skill.name] === false) { continue; } @@ -872,6 +887,16 @@ ${agentInstructions} } private resolveSkillPath(skillPath: string): string { + if (skillPath.startsWith("bundled:")) { + const relativePath = skillPath.slice("bundled:".length); + const root = this.getBundledSkillsRoot(); + const resolvedPath = path.resolve(root, relativePath); + const resolvedRoot = path.resolve(root); + if (resolvedPath === resolvedRoot || !resolvedPath.startsWith(`${resolvedRoot}${path.sep}`)) { + return path.join(root, "__invalid_bundled_skill__"); + } + return resolvedPath; + } if (skillPath.startsWith("~/")) { return path.join(os.homedir(), skillPath.slice(2)); } diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index e22067fe..abebed3c 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -471,6 +471,57 @@ test("SessionManager lists skills from Deep Code and .agents roots by priority", assert.equal(sharedSkill?.description, "Project .deepcode skill"); }); +test("SessionManager lists bundled skills at lowest priority", async () => { + const workspace = createTempDir("deepcode-bundled-skills-workspace-"); + const home = createTempDir("deepcode-bundled-skills-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-bundled-skills"); + const skills = await manager.listSkills(); + const skillWriter = skills.find((skill) => skill.name === "skill-writer"); + const selfRefer = skills.find((skill) => skill.name === "deepcode-self-refer"); + + assert.equal(skillWriter?.path, "bundled:skill-writer/SKILL.md"); + assert.equal(selfRefer?.path, "bundled:deepcode-self-refer/SKILL.md"); + assert.match(skillWriter?.description ?? "", /Guide users through creating/); +}); + +test("SessionManager lets project skills override bundled skills", async () => { + const workspace = createTempDir("deepcode-bundled-override-workspace-"); + const home = createTempDir("deepcode-bundled-override-home-"); + setHomeDir(home); + + const projectSkillDir = path.join(workspace, ".deepcode", "skills", "skill-writer"); + fs.mkdirSync(projectSkillDir, { recursive: true }); + fs.writeFileSync( + path.join(projectSkillDir, "SKILL.md"), + "---\nname: skill-writer\ndescription: Project override skill writer\n---\n# Project Skill Writer\n", + "utf8" + ); + + const manager = createSessionManager(workspace, "machine-id-bundled-override"); + const skillWriter = (await manager.listSkills()).find((skill) => skill.name === "skill-writer"); + + assert.equal(skillWriter?.path, "./.deepcode/skills/skill-writer/SKILL.md"); + assert.equal(skillWriter?.description, "Project override skill writer"); +}); + +test("SessionManager resolves bundled skill prompts", () => { + const workspace = createTempDir("deepcode-bundled-prompt-workspace-"); + const home = createTempDir("deepcode-bundled-prompt-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-bundled-prompt"); + const prompt = (manager as any).buildSkillPrompt({ + name: "skill-writer", + path: "bundled:skill-writer/SKILL.md", + description: "Write skills", + }); + + assert.match(prompt, / { const workspace = createTempDir("deepcode-disabled-skills-workspace-"); const home = createTempDir("deepcode-disabled-skills-home-"); @@ -511,6 +562,8 @@ test("SessionManager excludes disabled skills by resolved skill name", async () enabledSkills: { "skill-writer": false, "renamed-disabled": false, + "deepcode-self-refer": false, + "skill-digester": false, "enabled-skill": true, }, }), @@ -2814,7 +2867,10 @@ test("SessionManager stores usage per model across model changes", async () => { const client = { chat: { completions: { - create: async () => { + create: async (request: any) => { + if (isSkillMatchingRequest(request)) { + return createSkillMatchingResponse(); + } const response = responses.shift(); assert.ok(response, "expected a queued chat response"); return response; @@ -3353,7 +3409,10 @@ function createNotifyingSessionManager( const client = { chat: { completions: { - create: async () => { + create: async (request: any) => { + if (isSkillMatchingRequest(request)) { + return createSkillMatchingResponse(); + } const response = responses.shift(); assert.ok(response, "expected a queued chat response"); if (response instanceof Error) { @@ -3391,7 +3450,10 @@ function createMockedClientSessionManager(projectRoot: string, responses: unknow const client = { chat: { completions: { - create: async () => { + create: async (request: any) => { + if (isSkillMatchingRequest(request)) { + return createSkillMatchingResponse(); + } const response = responses.shift(); assert.ok(response, "expected a queued chat response"); return response; @@ -3427,7 +3489,10 @@ function createPermissionSessionManager( const client = { chat: { completions: { - create: async () => { + create: async (request: any) => { + if (isSkillMatchingRequest(request)) { + return createSkillMatchingResponse(); + } const response = responses.shift(); assert.ok(response, "expected a queued chat response"); return response; @@ -3467,6 +3532,14 @@ function createMockedClientSessionManagerWithClient(projectRoot: string, client: class APIUserAbortError extends Error {} +function isSkillMatchingRequest(request: any): boolean { + return request?.response_format?.type === "json_object"; +} + +function createSkillMatchingResponse(): unknown { + return { choices: [{ message: { content: '{"skillNames":[]}' } }] }; +} + function createChatResponse(content: string, usage: Record): unknown { return { choices: [{ message: { content } }], diff --git a/templates/skills/bundled/deepcode-self-refer/SKILL.md b/templates/skills/bundled/deepcode-self-refer/SKILL.md new file mode 100644 index 00000000..96f275f8 --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/SKILL.md @@ -0,0 +1,125 @@ +--- +name: deepcode-self-refer +description: 回答关于 Deep Code CLI 本身的问题——包括功能特性、配置项、斜杠命令、Skills、MCP 集成、权限、通知、会话持久化及故障排查。当用户询问如何配置或使用 Deep Code、如何设置 MCP 服务器、配置通知(如 Slack/飞书)、管理权限、查看可用技能、理解斜杠命令、配置思考模式、使用 Undo 功能,或咨询 Deep Code 与 VSCode 集成等场景时使用。 +--- + +# Deep Code Self-Refer + +This Skill helps you answer user questions about Deep Code CLI itself by consulting the reference documentation bundled with this Skill. All docs live in the `references/` subdirectory — always refer to them for authoritative answers. + +## When to use this Skill + +Use this Skill when the user asks any question about Deep Code itself, such as: + +- "列出可用的 skills" +- "如何配置 MCP?" +- "给当前项目配置 playwright mcp" +- "怎么启用搜索功能?" +- "支持哪些模型?" +- "如何配置思考模式?" +- "怎么设置权限?" +- "任务完成后怎么发通知?" +- "支持哪些斜杠命令?" +- "会话历史保存在哪里?" +- "/undo 是怎么工作的?" +- "Deep Code 和 VSCode 插件怎么配合?" +- Any other question about Deep Code CLI's features, configuration, or usage. + +## Instructions + +### Step 1: Identify the topic + +Map the user's question to the appropriate document(s): + +| Topic | Document | Key contents | +|-------|----------|-------------| +| **Overview, features, quick start** | `references/README.md` | Installation, slash commands, keyboard shortcuts, supported models, FAQ | +| **Configuration & settings** | `references/configuration.md` | `settings.json` fields, config hierarchy, env vars, thinking mode, reasoning effort, webSearchTool, enabledSkills | +| **MCP setup & usage** | `references/mcp.md` | MCP server config format, GitHub/Playwright/Filesystem examples, tool naming (`mcp____`), troubleshooting | +| **Permissions** | `references/permission.md` | Permission scopes (10 types), allow/deny/ask/defaultMode config, priority rules, persistence | +| **Notifications** | `references/notify.md` | Notify script path, injected env vars, Slack/Feishu/iTerm2/macOS/Linux/Windows examples | +| **Session persistence** | `references/session-persistence.md` | Storage paths, JSONL format, session index, compaction, `/undo` mechanics, code snapshots | + +### Step 2: Read the relevant document(s) + +Use the `Read` tool to read the appropriate document(s) from the list above. All paths are relative to this Skill's loaded root directory, where the `references/` subdirectory lives. + +- If the question spans multiple topics, read multiple documents. +- If a document doesn't exist in the user's preferred language (e.g., Chinese), try the other language variant (e.g., `references/configuration_en.md`). +- When answering from references/README.md, focus on the relevant sections. + +### Step 3: Answer with precision + +- **Quote the doc directly** for config examples, JSON snippets, or command syntax. +- **Don't guess** — if the answer isn't in the docs, say so and suggest checking GitHub Issues. +- **Provide copy-paste-ready configurations** when the user asks to set something up (e.g., MCP servers, notify scripts, permissions). +- **Mention related docs** when appropriate (e.g., MCP setup references `references/mcp.md`, the permissions section references `references/permission.md`). + +### Step 4: Handle common request patterns + +**"列出/查看可用的 skills":** +- Explain the skill scanning paths from references/README.md (`./.deepcode/skills/`, `./.agents/skills/`, `~/.deepcode/skills/`, `~/.agents/skills/`, and bundled built-in skills) +- Explain that `/skills` slash command lists available skills +- Mention `enabledSkills` in `settings.json` for enabling/disabling specific skills + +**"配置 MCP":** +- Read `references/mcp.md` for the MCP format and examples +- Ask the user for any required credentials (e.g., GitHub token) +- Provide the exact `mcpServers` JSON block to add to `settings.json` +- Mention using `/mcp` to verify the setup afterwards + +**"如何配置/修改 <设置项>":** +- Read `references/configuration.md` +- Explain which `settings.json` field controls the setting +- Clarify user-level (`~/.deepcode/settings.json`) vs project-level (`.deepcode/settings.json`) +- Provide the exact JSON snippet + +**"<斜杠命令> 是做什么的?":** +- Read the slash command table from references/README.md +- Provide a brief explanation with any additional context from relevant docs + +### Best practices + +1. **Always consult the docs first** — never answer from memory alone; the docs are the source of truth. +2. **Provide copy-paste-ready JSON** — users want to copy config blocks directly into their `settings.json`. +3. **Be specific about file paths** — always specify whether it's `~/.deepcode/settings.json` or `.deepcode/settings.json`. +4. **Mention `/mcp` verification** — after any MCP configuration change, remind users to use `/mcp` to verify. +5. **Acknowledge both Chinese and English docs** — the project has docs in both languages (`references/xxx.md` for Chinese, `references/xxx_en.md` for English). + +## Examples + +### Example 1: "列出可用的skills" + +Read references/README.md, locate the Skills section. Answer: + +- Skills are discovered from: `./.deepcode/skills/`, `./.agents/skills/`, `~/.deepcode/skills/`, `~/.agents/skills/`, and bundled built-in skills such as `bundled:deepcode-self-refer/SKILL.md`. +- Use `/skills` slash command in the Deep Code CLI to list all available skills +- Use `enabledSkills` in `settings.json` to enable/disable skills by name + +### Example 2: "给当前项目配置playwright mcp" + +Read `references/mcp.md`, locate the Playwright example. Answer: + +- Add to `settings.json` (user-level `~/.deepcode/settings.json` or project-level `.deepcode/settings.json`): + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest"] + } + } +} +``` + +- If merging with existing config, add the `"playwright"` entry into the existing `mcpServers` object +- After saving, use `/mcp` in Deep Code to verify the server is running + +### Example 3: "怎么设置通知到Slack?" + +Read `references/notify.md`, locate the Slack section. Answer with the script + config. + +### Example 4: "如何只允许AI读写当前目录?" + +Read `references/permission.md`, locate the strict mode example. Provide the exact JSON. diff --git a/templates/skills/bundled/deepcode-self-refer/references/README.md b/templates/skills/bundled/deepcode-self-refer/references/README.md new file mode 100644 index 00000000..de34da9a --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/README.md @@ -0,0 +1,209 @@ +
+
+
+

+ + deepcode-cli + +

+

Deep Code CLI

+ +[![][npm-release-shield]][npm-release-link] [![][npm-downloads-shield]][npm-downloads-link] [![][github-contributors-shield]][github-contributors-link] [![][github-forks-shield]][github-forks-link] [![][github-stars-shield]][github-stars-link] +[![][github-issues-shield]][github-issues-link] [![][github-issues-pr-shield]][github-issues-pr-link] [![][github-license-shield]][github-license-link] + +[English](README-en.md) · 中文 + +
+
+ +[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制、Agent Skills 以及 MCP 集成。 + +## 安装 + +```bash +npm install -g @vegamo/deepcode-cli +``` + +在任意项目目录下运行 `deepcode` 即可启动。 + +![intro2](resources/intro2.png) + +## 配置 + +创建 `~/.deepcode/settings.json` 文件,内容如下: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max" +} +``` + +配置文件与 [Deep Code VSCode 插件](https://github.com/lessweb/deepcode) 共享,无需重复配置。 + +完整配置说明(多层级优先级、环境变量等)请参阅 [docs/configuration.md](docs/configuration.md)。 + +## 主要功能 + +### **Skills** +Deep Code CLI 支持 agent skills,允许您扩展助手的能力: + +Skills 会按以下优先级扫描: + +| Scope | Path | Purpose | +| :------ | :-------------------- | :---------------------------- | +| Project | `./.deepcode/skills/` | Deep Code 原生位置,最高优先级 | +| Project | `./.agents/skills/` | 跨客户端互操作 | +| User | `~/.deepcode/skills/` | Deep Code 原生位置 | +| User | `~/.agents/skills/` | 跨客户端互操作 | +| Bundled | `bundled:/SKILL.md` | Deep Code 内置 skills,最低优先级 | + +### **为 DeepSeek 优化** +- 专门为 DeepSeek 模型性能调优。 +- 通过使用[上下文缓存](https://api-docs.deepseek.com/guides/kv_cache)来降低成本。 +- 原生支持[思考模式](https://api-docs.deepseek.com/guides/thinking_mode)和思考强度控制。 + +## 斜杠命令与按键功能 + +| 斜杠命令 | 操作 | +|-------------|----------------------------------| +| `/` | 打开 skills / 命令菜单 | +| `/new` | 开始新对话 | +| `/resume` | 选择历史对话继续 | +| `/continue` | 继续当前对话,或选择历史对话恢复 | +| `/model` | 切换模型、思考模式和推理强度 | +| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯) | +| `/init` | 初始化 AGENTS.md 文件 | +| `/skills` | 列出可用 skills | +| `/mcp` | 查看 MCP 服务器状态和可用工具 | +| `/undo` | 将代码和/或对话恢复到之前的状态 | +| `/exit` | 退出(也可用连续 `Ctrl+D`) | + +| 按键 | 操作 | +|---------------|--------------------| +| `Enter` | 发送消息 | +| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | +| `Ctrl+V` | 从剪贴板粘贴图片 | +| `Esc` | 中断当前模型回复 | +| 连续 `Ctrl+D` | 退出 | + +## 支持的模型 + +- `deepseek-v4-pro`(推荐使用) +- `deepseek-v4-flash` +- 任何其他 OpenAI 兼容模型 + + +## 常见问题 + +### Deep Code 是否有 VSCode 插件? + +有的。Deep Code 提供功能完整的 VSCode 插件,可在 [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=vegamo.deepcode-vscode) 安装。插件与 CLI 共享 `~/.deepcode/settings.json` 配置文件,可以在终端和编辑器之间无缝切换。 + +### Deep Code 是否支持理解图片? + +Deep Code 支持多模态,可使用ctrl+v从剪贴板粘贴图片。但目前 deepseek-v4 不支持多模态。有些模型虽然有多模态能力,但对多轮对话请求的限制太严。目前多模态输入推荐使用火山方舟的 Doubao-Seed-2.0-pro 模型,适配效果最好。 + +### 怎样在任务完成后自动给 Slack 发消息? + +编写一个调用 Slack webhook 的 Shell 通知脚本,然后在 `~/.deepcode/settings.json` 中将 `notify` 字段设为该脚本的完整路径即可。详细步骤请参考 [docs/notify.md](docs/notify.md)。 + +### 怎样启用联网搜索功能? + +Deep Code自带免费的、且大部分情况够用的Web Search工具。如果你希望使用自定义脚本进行联网搜索,可以在 `~/.deepcode/settings.json` 中将 `webSearchTool` 设为脚本的完整路径即可。详细步骤可参考:https://github.com/qorzj/web_search_cli + +### 如何配置 MCP? + +Deep Code 支持 MCP(Model Context Protocol),可以连接 GitHub、浏览器、数据库等外部服务。在 `settings.json` 中配置 `mcpServers` 字段即可启用,启动后使用 `/mcp` 命令查看已配置的 MCP 服务器状态和可用工具。 + +详细配置指南:[docs/mcp.md](docs/mcp.md) + +### 如何配置 Deep Code 任务完成后发送通知? + +当 AI 助手完成一轮任务后,Deep Code 可以自动执行一个通知脚本,将任务结果发送到你指定的渠道(如 Slack、系统通知等)。 + +详细配置指南:[docs/notify.md](docs/notify.md) + +### Deep Code 只支持 YOLO 模式吗? + +不是。Deep Code 内置了细粒度的权限控制机制,支持在 AI 助手执行 Shell 命令、读写文件、访问网络等操作前进行确认。你可以通过 `settings.json` 中的 `permissions` 字段按需配置每种权限范围的策略:始终允许、始终询问、或直接拒绝。详见 [docs/permission.md](docs/permission.md)。 + +### 是否支持 Coding Plan? + +支持。只要把 `~/.deepcode/settings.json` 的 `env.BASE_URL` 配置为 OpenAI 兼容的接口地址就行。以火山方舟的 Coding Plan 为例: + +```json +{ + "env": { + "MODEL": "ark-code-latest", + "BASE_URL": "https://ark.cn-beijing.volces.com/api/coding/v3", + "API_KEY": "**************" + }, + "thinkingEnabled": true +} +``` + +## 贡献 + +欢迎贡献代码!以下是参与方式: + +```bash +# 克隆仓库 +git clone https://github.com/lessweb/deepcode-cli.git +cd deepcode-cli + +# 安装依赖 +npm install + +# 本地开发(类型检查 + lint + 格式检查 + 构建) +npm run build + +# 运行测试 +npm test + +# 链接到全局(即本地全局安装) +npm link +``` + +- 提交 PR 前请确保 `npm run check` 通过(类型检查 + lint + 格式检查) +- 建议在执行构建前,先执行 `npm run format` 自动格式化代码,避免构建报错 + +## 获取帮助 + +- 在 GitHub Issues 上报告错误或请求功能 (https://github.com/lessweb/deepcode-cli/issues) + +## 协议 + +- MIT + +## 支持我们 + +如果你觉得这个工具对你有帮助,请考虑通过以下方式支持我们: + +- 在 GitHub 上给我们一个 Star (https://github.com/lessweb/deepcode-cli) +- 向我们提交反馈和建议 +- 分享给你的朋友和同事 + + + +[npm-release-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli +[npm-release-shield]: https://img.shields.io/npm/v/@vegamo/deepcode-cli?color=4d6BFE&labelColor=black&logo=npm&logoColor=white&style=flat-square&cacheSeconds=1800 +[npm-downloads-link]: https://www.npmjs.com/package/@vegamo/deepcode-cli +[npm-downloads-shield]: https://img.shields.io/npm/dt/@vegamo/deepcode-cli?labelColor=black&style=flat-square&color=4d6BFE&cacheSeconds=1800 +[github-contributors-link]: https://github.com/lessweb/deepcode-cli/graphs/contributors +[github-contributors-shield]: https://img.shields.io/github/contributors/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 +[github-forks-link]: https://github.com/lessweb/deepcode-cli/network/members +[github-forks-shield]: https://img.shields.io/github/forks/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 +[github-stars-link]: https://github.com/lessweb/deepcode-cli/network/stargazers +[github-stars-shield]: https://img.shields.io/github/stars/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 +[github-issues-link]: https://github.com/lessweb/deepcode-cli/issues +[github-issues-shield]: https://img.shields.io/github/issues/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 +[github-issues-pr-link]: https://github.com/lessweb/deepcode-cli/pulls +[github-issues-pr-shield]: https://img.shields.io/github/issues-pr/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 +[github-license-link]: https://github.com/lessweb/deepcode-cli/blob/main/LICENSE +[github-license-shield]: https://img.shields.io/github/license/lessweb/deepcode-cli?color=4d6BFE&labelColor=black&style=flat-square&cacheSeconds=1800 diff --git a/templates/skills/bundled/deepcode-self-refer/references/configuration.md b/templates/skills/bundled/deepcode-self-refer/references/configuration.md new file mode 100644 index 00000000..2f198b10 --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/configuration.md @@ -0,0 +1,217 @@ +# Deep Code 配置 + +## 配置层级 + +配置按以下优先级顺序应用(数字较小的会被数字较大的覆盖): + +| 层级 | 配置来源 | 说明 | +| ---- | ------------ | ------------------------------------------- | +| 1 | 默认值 | 应用程序内硬编码的默认值 | +| 2 | 用户设置文件 | 当前用户的全局设置 | +| 3 | 项目设置文件 | 项目特定的设置 | +| 4 | 环境变量 | 系统范围或会话特定的变量 | + +## 设置文件 + +Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两个层级的存放位置: + +| 文件类型 | 位置 | 作用范围 | +| ------------ | ---------------------------------- | ---------------------------------------------------- | +| 用户设置文件 | `~/.deepcode/settings.json` | 适用于当前用户的所有 Deep Code 会话。 | +| 项目设置文件 | `项目根目录/.deepcode/settings.json` | 仅在该特定项目中运行 Deep Code 时生效。项目设置会覆盖用户设置。 | + +### `settings.json` 中的可用设置 + +以下是 `settings.json` 支持的全部顶层字段,以及 `env` 内部支持的子字段: + +| 字段 | 类型 | 说明 | +| -------------------- | --------- | ------------------------------------------------------------------- | +| `env` | object | 环境变量分组(见下方子字段表) | +| `model` | string | 模型名称。优先级高于 `env.MODEL` | +| `thinkingEnabled` | boolean | 是否启用思考模式(DeepSeek V4 系列默认启用) | +| `reasoningEffort` | string | 推理强度,可选 `"high"` 或 `"max"`(默认 `"max"`) | +| `debugLogEnabled` | boolean | 是否启用调试日志输出(默认 `false`) | +| `telemetryEnabled` | boolean | 是否启用匿名使用数据上报(默认 `true`) | +| `notify` | string | 任务完成通知脚本的完整路径(如 Slack 通知脚本) | +| `webSearchTool` | string | 自定义联网搜索脚本的完整路径 | +| `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) | +| `temperature` | number | 模型采样温度,范围 `0` 到 `2` | +| `enabledSkills` | object | 按 skill 名称启用或禁用 skill 的配置 | + +#### `env` 子字段 + +| 字段 | 类型 | 说明 | +| ---------- | ------ | ------------------------------------------------------------------ | +| `MODEL` | string | 模型名称。例如 `"deepseek-v4-pro"`、`"deepseek-v4-flash"` | +| `BASE_URL` | string | API 请求的基础 URL。例如 `"https://api.deepseek.com"` | +| `API_KEY` | string | API 密钥 | +| `TEMPERATURE` | string | Chat Completions 采样温度,范围 `"0"` 到 `"2"` | +| `THINKING_ENABLED` | string | 是否启用思考模式 | +| `REASONING_EFFORT` | string | 推理强度 | +| `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 | +| `TELEMETRY_ENABLED` | string | 是否启用匿名使用数据上报 | +| `<其他任意KEY>` | string | 自定义环境变量 | + +#### `thinkingEnabled` — 思考模式 + +是否启用 DeepSeek 思考模式。设置为 `true` 启用、`false` 禁用。 + +- 对于 `deepseek-v4-pro` 和 `deepseek-v4-flash`,思考模式**默认启用**。 +- 对于其他模型,思考模式**默认关闭**。 + +#### `reasoningEffort` — 推理强度 + +当思考模式启用时,控制模型思考的深度: + +| 值 | 说明 | +| ------ | --------------------------------- | +| `max` | 最大推理深度(默认值) | +| `high` | 较高推理深度,token消耗相对较小 | + +#### `notify` — 任务完成通知 + +设置一个 Shell 脚本的完整路径。当 AI 助手完成一轮任务后,会自动执行该脚本,可用于发送通知(如 Slack 消息)。 + +通知脚本执行时,会通过环境变量注入以下上下文信息: + +| 环境变量 | 说明 | +|----------|------| +| `DURATION` | 会话耗时,单位秒(整数) | +| `STATUS` | 会话状态:`"completed"` 或 `"failed"` | +| `FAIL_REASON` | 失败原因(仅失败时设置) | +| `BODY` | 最后一条 AI 助手回复的文本内容 | +| `TITLE` | 会话标题(对应 resume 列表中的标题) | + +```json +{ + "notify": "/path/to/notify-script.sh" +} +``` + +> 详细的 Slack、飞书、终端通知、系统通知等配置示例,请参阅 [notify.md](notify.md)。 + +#### `webSearchTool` — 自定义联网搜索 + +Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索逻辑,可将 `webSearchTool` 设为一个可执行脚本的完整路径: + +```json +{ + "webSearchTool": "/path/to/my-search-script.sh" +} +``` + +脚本接收一个搜索查询参数,输出 JSON 格式的结果供 AI 使用。 + +#### `enabledSkills` — Skill 启用配置 + +控制 skill 扫描时是否包含指定 skill。键是解析后的 skill 名称,值必须是布尔值: + +```json +{ + "enabledSkills": { + "skill-writer": false, + "code-review": true + } +} +``` + +- 未配置的 skill 默认启用。 +- 将某个 skill 设置为 `false` 后,所有项目级和用户级目录中解析名称相同的 skill 都会被隐藏。 +- 项目设置会按 skill 覆盖用户设置。如果项目设置没有配置某个 skill,则使用用户设置。 + +#### `mcpServers` — MCP 服务器 + +MCP(Model Context Protocol)服务器配置。值是键值对,键为服务名称,值为服务器配置对象。 + +```json +{ + "mcpServers": { + "<服务名>": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx" + } + } + } +} +``` + +| McpServerConfig 字段 | 类型 | 必填 | 说明 | +| -------------------- | -------- | ---- | -------------------------------------------------------------------- | +| `command` | string | 是 | 可执行文件路径或命令(如 `npx`、`node`、`python`) | +| `args` | string[] | 否 | 传递给命令的参数列表 | +| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量 | + +> 当 `command` 为 `npx` 时,Deep Code 会自动在参数前补充 `-y`。 + +详细 MCP 使用说明请参考 [mcp.md](mcp.md)。 + + +#### `debugLogEnabled` — 调试日志 + +设为 `true` 可让程序输出详细的调试日志(默认 `false`),用于排查 API 调用和工具执行的问题。 + +#### `telemetryEnabled` — 匿名使用数据上报 + +设为 `false` 可关闭匿名使用数据上报(默认 `true`)。上报仅包含匿名的机器标识,不包含对话内容、代码或 API 密钥。 + +也可以通过环境变量关闭: + +```bash +DEEPCODE_TELEMETRY_ENABLED=0 deepcode +``` + +## 环境变量优先级 + +环境变量是配置应用程序的常用方式,尤其适用于敏感信息(如 api-key)或可能在不同环境之间更改的设置。 + +### 优先级原则 + +环境变量优先级遵循“越具体、越局部的配置,优先级越高”和“env文件默认保护现有环境,系统变量高于env文件”的覆盖逻辑。(settings.json的env对象可以认为是一种env文件) + +优先级层级 (由低到高) +1. settings.json 外层的 env:这是针对整个工具及其所有子进程的通用配置(全局变量)。可被外层环境变量覆盖,但环境变量KEY会移除`DEEPCODE_`前缀。 +2. settings.json mcpServers 内定义的 env:这是针对特定 MCP 服务的最具体配置(局部变量)。可被外层环境变量覆盖,但环境变量KEY会移除`MCP_`前缀。 +3. Shell 环境系统变量:操作系统层面的环境变量。 + +### 场景 + +#### 一、设置模型的api_key, base_url + +按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以api_key为例): + +1. 硬编码默认值: `""` +2. 用户级settings.json: `{"env": {"API_KEY": "abc123"}}` +3. 项目级settings.json: `{"env": {"API_KEY": "abc123"}}` +4. 系统环境变量: `DEEPCODE_API_KEY=abc123 deepcode` + +#### 二、设置模型的model, thinkingEnabled, reasoningEffort + +按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以thinkingEnabled为例): + +1. 硬编码默认值: `true` +2. 用户级settings.json: `{"env": {"THINKING_ENABLED": "true"}}` +3. 用户级settings.json: `{"thinkingEnabled": true}` +4. 项目级settings.json: `{"env": {"THINKING_ENABLED": "true"}}` +5. 项目级settings.json: `{"thinkingEnabled": true}` +6. 系统环境变量: `DEEPCODE_THINKING_ENABLED=true deepcode` + +#### 三、设置启动notify, webSearchTool等外挂脚本的环境变量 + +按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以notify为例): + +1. 硬编码默认值:`os.environ.get('WEBHOOK', '...') # notify脚本代码` +2. 用户级settings.json: `{"env": {"WEBHOOK": "..."}}` +3. 项目级settings.json: `{"env": {"WEBHOOK": "true"}}` +4. 系统环境变量: `DEEPCODE_WEBHOOK=... deepcode` + +#### 四、设置MCP Service的环境变量 + +按以下优先级顺序应用(数字较小的会被数字较大的覆盖)(以github MCP server为例): + +1. 用户级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}` +2. 用户级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}` +3. 项目级settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}` +4. 项目级settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}` +5. 系统环境变量: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode` diff --git a/templates/skills/bundled/deepcode-self-refer/references/configuration_en.md b/templates/skills/bundled/deepcode-self-refer/references/configuration_en.md new file mode 100644 index 00000000..fac8c349 --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/configuration_en.md @@ -0,0 +1,216 @@ +# Deep Code Configuration + +## Configuration Hierarchy + +Configuration is applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones): + +| Layer | Configuration Source | Description | +| ----- | -------------------- | ---------------------------------------------- | +| 1 | Defaults | Hardcoded defaults within the application | +| 2 | User settings file | Global settings for the current user | +| 3 | Project settings file| Project-specific settings | +| 4 | Environment variables| System-wide or session-specific variables | + +## Settings File + +Deep Code uses the `settings.json` file for persistent configuration, supporting two storage locations: + +| File Type | Location | Scope | +| ------------------- | ----------------------------------------- | --------------------------------------------------------------------- | +| User settings file | `~/.deepcode/settings.json` | Applies to all Deep Code sessions for the current user. | +| Project settings file | `/.deepcode/settings.json` | Takes effect only when running Deep Code in that specific project. Project settings override user settings. | + +### Available Settings in `settings.json` + +The following are all the top-level fields supported in `settings.json`, along with the sub-fields inside `env`: + +| Field | Type | Description | +| ------------------ | ------- | --------------------------------------------------------------------------- | +| `env` | object | Group of environment variables (see sub-field table below) | +| `model` | string | Model name. Takes precedence over `env.MODEL` | +| `thinkingEnabled` | boolean | Whether to enable thinking mode (enabled by default for DeepSeek V4 series)| +| `reasoningEffort` | string | Reasoning intensity, either `"high"` or `"max"` (default `"max"`) | +| `debugLogEnabled` | boolean | Enable debug log output (default `false`) | +| `telemetryEnabled` | boolean | Enable anonymous usage reporting (default `true`) | +| `notify` | string | Full path to a task-completion notification script (e.g., Slack notification script) | +| `webSearchTool` | string | Full path to a custom web search script | +| `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) | +| `temperature` | number | Sampling temperature for LLM, from `0` to `2` | +| `enabledSkills` | object | Per-skill enable/disable map, keyed by skill name | + +#### `env` Sub-fields + +| Field | Type | Description | +| ----------------- | ------ | ---------------------------------------------------------------- | +| `MODEL` | string | Model name, e.g. `"deepseek-v4-pro"`, `"deepseek-v4-flash"` | +| `BASE_URL` | string | Base URL for API requests, e.g. `"https://api.deepseek.com"` | +| `API_KEY` | string | API key | +| `TEMPERATURE` | string | Sampling temperature for chat completions, from `"0"` to `"2"` | +| `THINKING_ENABLED`| string | Enable thinking mode | +| `REASONING_EFFORT`| string | Reasoning intensity | +| `DEBUG_LOG_ENABLED`| string| Enable debug log output | +| `TELEMETRY_ENABLED`| string| Enable anonymous usage reporting | +| `` | string | Custom environment variable | + +#### `thinkingEnabled` — Thinking Mode + +Whether to enable DeepSeek thinking mode. Set to `true` to enable, `false` to disable. + +- For `deepseek-v4-pro` and `deepseek-v4-flash`, thinking mode is **enabled by default**. +- For other models, thinking mode is **disabled by default**. + +#### `reasoningEffort` — Reasoning Intensity + +When thinking mode is enabled, controls the depth of the model’s reasoning: + +| Value | Description | +| ------ | --------------------------------------------------------- | +| `max` | Maximum reasoning depth (default) | +| `high` | Higher reasoning depth with relatively lower token usage | + +#### `notify` — Task Completion Notification + +Set a full path to a shell script. When the AI assistant finishes a round of tasks, the script is executed automatically, which can be used to send notifications (e.g., a Slack message). + +The following context is injected as environment variables when the notify script runs: + +| Variable | Description | +|----------|-------------| +| `DURATION` | Session duration in seconds (integer) | +| `STATUS` | Session status: `"completed"` or `"failed"` | +| `FAIL_REASON` | Failure reason (only set on failure) | +| `BODY` | The text content of the last AI assistant reply | +| `TITLE` | Session title (matches the resume list title) | + +```json +{ + "notify": "/path/to/notify-script.sh" +} +``` + +> For detailed configuration examples (Slack, Feishu, terminal notifications, system notifications, etc.), see [notify_en.md](notify_en.md). + +#### `webSearchTool` — Custom Web Search + +Deep Code has a built-in, free-to-use Web Search tool. If you need custom search logic, set `webSearchTool` to the full path of an executable script: + +```json +{ + "webSearchTool": "/path/to/my-search-script.sh" +} +``` + +The script receives a search query as an argument and outputs results in JSON format for the AI. + +#### `enabledSkills` — Skill Enablement + +Controls whether skills are included during skill scanning. Keys are resolved skill names, and values must be booleans: + +```json +{ + "enabledSkills": { + "skill-writer": false, + "code-review": true + } +} +``` + +- Missing entries are enabled by default. +- Setting a skill to `false` hides every skill with that resolved `name`, across project and user skill roots. +- Project settings override user settings per skill. If the project setting omits a skill, the user setting is used. + +#### `mcpServers` — MCP Servers + +Configuration for MCP (Model Context Protocol) servers. The value is a key-value pair, where the key is the service name and the value is a server configuration object. + +```json +{ + "mcpServers": { + "": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx" + } + } + } +} +``` + +| McpServerConfig field | Type | Required | Description | +| --------------------- | -------- | -------- | ------------------------------------------------------------------------ | +| `command` | string | Yes | Executable path or command (e.g. `npx`, `node`, `python`) | +| `args` | string[] | No | List of arguments passed to the command | +| `env` | object | No | Environment variables passed to the MCP server process | + +> When `command` is `npx`, Deep Code automatically prepends `-y` to the arguments. + +For detailed MCP usage instructions, refer to [mcp.md](mcp.md). + +#### `debugLogEnabled` — Debug Log + +Set to `true` to enable detailed debug logging (default `false`), useful for troubleshooting API calls and tool execution. + +#### `telemetryEnabled` — Anonymous Usage Reporting + +Set to `false` to disable anonymous usage reporting (default `true`). The report only includes an anonymous machine identifier and does not contain conversation content, code, or API keys. + +You can also disable it via environment variable: + +```bash +DEEPCODE_TELEMETRY_ENABLED=0 deepcode +``` + +## Environment Variable Priority + +Environment variables are a common way to configure applications, especially for sensitive information (such as api-key) or settings that may change between environments. + +### Priority Principle + +Environment variable priority follows the logic of “the more specific and localized the configuration, the higher the priority”, and the override rule of “env files protect existing environment by default, system variables override env files”. (The `env` object in settings.json can be thought of as a type of env file.) + +Priority levels (from lowest to highest): +1. `env` defined at the top level of `settings.json` – this is a general configuration for the entire tool and all its subprocesses (global variables). Can be overridden by outer environment variables, but the environment variable KEY has the `DEEPCODE_` prefix removed. +2. `env` defined inside `mcpServers` in `settings.json` – this is the most specific configuration for a particular MCP service (local variables). Can be overridden by outer environment variables, but the KEY has the `MCP_` prefix removed. +3. Shell/system environment variables – operating system level. + +### Scenarios + +#### 1. Setting the model’s api_key and base_url + +Applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones) – using api_key as an example: + +1. Hardcoded default: `""` +2. User-level settings.json: `{"env": {"API_KEY": "abc123"}}` +3. Project-level settings.json: `{"env": {"API_KEY": "abc123"}}` +4. System environment variable: `DEEPCODE_API_KEY=abc123 deepcode` + +#### 2. Setting model, thinkingEnabled, and reasoningEffort + +Applied in the following priority order (lower-numbered overridden by higher-numbered) – using thinkingEnabled as an example: + +1. Hardcoded default: `true` +2. User-level settings.json: `{"env": {"THINKING_ENABLED": "true"}}` +3. User-level settings.json: `{"thinkingEnabled": true}` +4. Project-level settings.json: `{"env": {"THINKING_ENABLED": "true"}}` +5. Project-level settings.json: `{"thinkingEnabled": true}` +6. System environment variable: `DEEPCODE_THINKING_ENABLED=true deepcode` + +#### 3. Setting environment variables for external scripts like notify and webSearchTool + +Applied in the following priority order (lower-numbered overridden by higher-numbered) – using notify as an example: + +1. Hardcoded default: `os.environ.get('WEBHOOK', '...') # notify script code` +2. User-level settings.json: `{"env": {"WEBHOOK": "..."}}` +3. Project-level settings.json: `{"env": {"WEBHOOK": "true"}}` +4. System environment variable: `DEEPCODE_WEBHOOK=... deepcode` + +#### 4. Setting environment variables for an MCP Service + +Applied in the following priority order (lower-numbered overridden by higher-numbered) – using a GitHub MCP server as an example: + +1. User-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}` +2. User-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}` +3. Project-level settings.json: `{"mcpServers":{"github":{"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"..."}}}}` +4. Project-level settings.json: `{"env": {"MCP_GITHUB_PERSONAL_ACCESS_TOKEN": "..."}}` +5. System environment variable: `DEEPCODE_MCP_GITHUB_PERSONAL_ACCESS_TOKEN=... deepcode` diff --git a/templates/skills/bundled/deepcode-self-refer/references/mcp.md b/templates/skills/bundled/deepcode-self-refer/references/mcp.md new file mode 100644 index 00000000..73034a38 --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/mcp.md @@ -0,0 +1,200 @@ +# Deep Code CLI MCP 配置指南 + +Deep Code CLI 支持 MCP(Model Context Protocol),让 AI 助手能够连接外部工具和服务,如 GitHub、浏览器、数据库等。 + +## 概述 + +配置 MCP 后,Deep Code 可以: + +- 操作 GitHub 仓库(查看 Issues、创建 PR、搜索代码等) +- 操控浏览器(截图、点击、填表单等) +- 访问文件系统 +- 连接数据库和 API +- ...以及任何兼容 MCP 协议的外部服务 + +MCP 工具在 Deep Code 中的命名格式为 `mcp__<服务名>__<工具名>`,例如 `mcp__github__search_code`。 + +## 配置 MCP 服务器 + +编辑 `~/.deepcode/settings.json`,添加 `mcpServers` 字段: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max", + "mcpServers": { + "<服务名称>": { + "command": "<可执行文件>", + "args": ["<参数1>", "<参数2>"], + "env": { + "<环境变量>": "<值>" + } + } + } +} +``` + +### 配置项说明 + +| 字段 | 类型 | 必填 | 说明 | +| --------- | -------- | ---- | ---------------------------------------------------------------------------------------------------------------------- | +| `command` | string | 是 | MCP 服务器的可执行文件路径或命令(如 `npx`、`node`、`python`)。当命令是 `npx` 时,Deep Code 会自动在参数前补充 `-y`。 | +| `args` | string[] | 否 | 传递给命令的参数列表 | +| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量(如 API Key) | + +## 常用 MCP 示例 + +### GitHub MCP + +让 Deep Code 直接操作 GitHub 仓库(搜索代码、管理 Issue/PR、读写文件等): + +```json +{ + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx" + } + } + } +} +``` + +> GitHub Personal Access Token 可在 [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) 生成。 + +### 浏览器控制(Playwright) + +让 Deep Code 操控浏览器进行截图、页面操作等: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest"] + } + } +} +``` + +### 文件系统 + +让 Deep Code 在指定目录中读写文件: + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"] + } + } +} +``` + +### 自定义 Python MCP + +```json +{ + "mcpServers": { + "my-tool": { + "command": "python", + "args": ["-m", "my_mcp_server"], + "env": { + "API_KEY": "xxx" + } + } + } +} +``` + +## 完整配置示例 + +以下是一个配置了 GitHub 和 Playwright 两个 MCP 服务器的完整 `~/.deepcode/settings.json`: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-xxxxxxxxxxxx" + }, + "thinkingEnabled": true, + "reasoningEffort": "max", + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx" + } + }, + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest"] + } + } +} +``` + +## 使用 MCP + +配置完成后,启动 `deepcode`,在聊天中输入 `/mcp` 即可查看所有已配置的 MCP 服务器状态以及每个服务器提供的工具列表。 + +在对话中直接使用 MCP 工具名称即可调用,例如: + +``` +帮我搜索 GitHub 上 deepcode-cli 仓库的 issues +``` + +AI 会自动调用 `mcp__github__search_issues` 工具完成操作。 + +## 工具命名规则 + +MCP 工具名称由三部分组成:`mcp__<服务名>__<工具名>` + +| 服务名 | 工具名 | 完整调用名 | +| ---------- | ----------------------- | ------------------------------------------ | +| github | search_code | `mcp__github__search_code` | +| github | create_pull_request | `mcp__github__create_pull_request` | +| playwright | browser_navigate | `mcp__playwright__browser_navigate` | +| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` | + +你可以通过 `/mcp` 查看每个服务器提供的具体工具列表。 + +## 故障排查 + +### 启动失败 + +如果 MCP 服务器无法启动,检查: + +1. `command` 是否已安装(如 `npx` 需要 Node.js) +2. `env` 中的环境变量是否正确(如 `GITHUB_PERSONAL_ACCESS_TOKEN`) +3. 运行 `deepcode` 的终端是否有网络访问权限 + +### 工具不显示 + +1. 确认 `settings.json` 中的 `mcpServers` 字段格式正确 +2. 启动 deepcode 后使用 `/mcp` 查看服务器状态 +3. 如果服务器状态显示错误,根据错误信息排查 + +### Windows 用户 + +在 Windows 上,Deep Code CLI 会自动为 `.cmd` 命令添加 shell 支持。如果你的 MCP 命令是批处理脚本,确保文件名以 `.cmd` 结尾。 + +## 编写你自己的 MCP 服务器 + +MCP 服务器遵循 [Model Context Protocol](https://modelcontextprotocol.io/) 规范,使用 JSON-RPC 2.0 通信。你可以用任何语言编写 MCP 服务器,只要实现以下协议即可: + +1. `initialize` — 握手和协议协商 +2. `tools/list` — 返回可用工具列表 +3. `tools/call` — 执行工具调用 + +更多参考:[MCP 官方文档](https://modelcontextprotocol.io/) diff --git a/templates/skills/bundled/deepcode-self-refer/references/mcp_en.md b/templates/skills/bundled/deepcode-self-refer/references/mcp_en.md new file mode 100644 index 00000000..03c4b30c --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/mcp_en.md @@ -0,0 +1,200 @@ +# Deep Code CLI MCP Configuration Guide + +Deep Code CLI supports MCP (Model Context Protocol), enabling AI assistants to connect with external tools and services such as GitHub, browsers, databases, and more. + +## Overview + +Once MCP is configured, Deep Code can: + +- Operate on GitHub repositories (view issues, create PRs, search code, etc.) +- Control browsers (screenshots, clicks, form filling, etc.) +- Access the file system +- Connect to databases and APIs +- ...and any external service compatible with the MCP protocol + +MCP tools are named in Deep Code using the format `mcp____`, for example `mcp__github__search_code`. + +## Configuring MCP Servers + +Edit `~/.deepcode/settings.json` and add the `mcpServers` field: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max", + "mcpServers": { + "": { + "command": "", + "args": ["", ""], + "env": { + "": "" + } + } + } +} +``` + +### Configuration Fields + +| Field | Type | Required | Description | +| --------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `command` | string | Yes | Path or command of the MCP server executable (e.g., `npx`, `node`, `python`). When the command is `npx`, Deep Code automatically prepends `-y` to the arguments. | +| `args` | string[] | No | List of arguments to pass to the command | +| `env` | object | No | Environment variables (e.g., API keys) to pass to the MCP server process | + +## Common MCP Examples + +### GitHub MCP + +Allows Deep Code to directly operate on GitHub repositories (search code, manage issues/PRs, read/write files, etc.): + +```json +{ + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx" + } + } + } +} +``` + +> Generate a GitHub Personal Access Token at [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens). + +### Browser Control (Playwright) + +Lets Deep Code control a browser for screenshots, page interactions, etc.: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest"] + } + } +} +``` + +### File System + +Enables Deep Code to read and write files within a specified directory: + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"] + } + } +} +``` + +### Custom Python MCP + +```json +{ + "mcpServers": { + "my-tool": { + "command": "python", + "args": ["-m", "my_mcp_server"], + "env": { + "API_KEY": "xxx" + } + } + } +} +``` + +## Full Configuration Example + +Below is a complete `~/.deepcode/settings.json` with both GitHub and Playwright MCP servers configured: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-xxxxxxxxxxxx" + }, + "thinkingEnabled": true, + "reasoningEffort": "max", + "mcpServers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx" + } + }, + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest"] + } + } +} +``` + +## Using MCP + +After configuration, start `deepcode` and type `/mcp` in the chat to view the status of all configured MCP servers and the list of tools each server provides. + +Simply use the MCP tool name in your conversation to invoke it, for example: + +``` +Help me search for issues in the deepcode-cli repository on GitHub +``` + +The AI will automatically invoke the `mcp__github__search_issues` tool to complete the action. + +## Tool Naming Convention + +An MCP tool name consists of three parts: `mcp____` + +| Service | Tool Name | Full Invocation Name | +| ---------- | ----------------------- | ------------------------------------------- | +| github | search_code | `mcp__github__search_code` | +| github | create_pull_request | `mcp__github__create_pull_request` | +| playwright | browser_navigate | `mcp__playwright__browser_navigate` | +| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` | + +You can view the list of tools provided by each server using `/mcp`. + +## Troubleshooting + +### Startup Failure + +If an MCP server fails to start, check: + +1. Whether `command` is installed (e.g., `npx` requires Node.js) +2. Whether environment variables in `env` are correct (e.g., `GITHUB_PERSONAL_ACCESS_TOKEN`) +3. Whether the terminal running `deepcode` has network access + +### Tools Not Showing Up + +1. Verify that the `mcpServers` field in `settings.json` is correctly formatted +2. After starting deepcode, use `/mcp` to check server status +3. If the server status shows an error, debug based on the error message + +### Windows Users + +On Windows, Deep Code CLI automatically adds shell support for `.cmd` commands. If your MCP command is a batch script, ensure the filename ends with `.cmd`. + +## Writing Your Own MCP Server + +MCP servers follow the [Model Context Protocol](https://modelcontextprotocol.io/) specification and communicate using JSON‑RPC 2.0. You can write an MCP server in any language as long as it implements the following methods: + +1. `initialize` — Handshake and protocol negotiation +2. `tools/list` — Return the list of available tools +3. `tools/call` — Execute a tool call + +For more information, see the [official MCP documentation](https://modelcontextprotocol.io/). \ No newline at end of file diff --git a/templates/skills/bundled/deepcode-self-refer/references/notify.md b/templates/skills/bundled/deepcode-self-refer/references/notify.md new file mode 100644 index 00000000..d73eef45 --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/notify.md @@ -0,0 +1,211 @@ +# Deep Code 任务完成通知 + +当 AI 助手完成一轮任务后,Deep Code 可以自动执行一个通知脚本,将任务结果发送到你指定的渠道(如 Slack、系统通知等)。 + +## 工作原理 + +在 `settings.json` 中配置 `notify` 字段,指向一个可执行脚本的完整路径。每次 AI 助手完成任务应答后,Deep Code 会执行该脚本,并通过环境变量注入上下文信息。 + +## 注入的环境变量 + +| 环境变量 | 说明 | +|----------|------| +| `DURATION` | 会话耗时,单位秒(整数) | +| `STATUS` | 会话状态:`"completed"` 或 `"failed"` | +| `FAIL_REASON` | 失败原因(仅失败时设置) | +| `BODY` | 最后一条 AI 助手回复的文本内容 | +| `TITLE` | 会话标题(对应 resume 列表中的标题) | + +## 配置方法 + +编辑 `~/.deepcode/settings.json`,添加 `notify` 字段: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max", + "notify": "/path/to/your-notify-script.sh" +} +``` + +你也可以在 `env` 中配置通知脚本所需的自定义环境变量,例如 Slack Webhook URL: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-...", + "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/*****/****/**********" + }, + "notify": "/Users/you/.deepcode/notify-slack.sh" +} +``` + +这些 `env` 中的变量会被注入到脚本的执行环境中。 + +## Slack 通知 + +### 1. 获取 Slack Webhook URL + +1. 创建 [Slack App](https://api.slack.com/apps) +2. 在 App 页面点击 **Incoming Webhooks** → **Add New Webhook to Workspace**,生成 Webhook URL + +### 2. 创建通知脚本 + +创建 `~/.deepcode/notify-slack.sh`: + +```bash +#!/usr/bin/env bash +SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}" +CURRENT_DIR=$(pwd) +BRANCH=$(git branch --show-current 2>/dev/null) +curl -X POST "$SLACK_WEBHOOK_URL" \ + -H "Content-type: application/json" \ + --data "{ + \"text\": \"✅ Deep Code 任务已完成\n · cwd: $CURRENT_DIR\n · Branch: $BRANCH\n · Duration: $DURATION 秒\" + }" +``` + +给脚本添加可执行权限: + +```bash +chmod +x ~/.deepcode/notify-slack.sh +``` + +### 3. 配置 settings.json + +```json +{ + "env": { + "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/*****/****/**********" + }, + "notify": "/Users/you/.deepcode/notify-slack.sh" +} +``` + +> Python 版本的脚本同样支持,你可以在 `env` 中传入并引用任意自定义环境变量。 + +## 飞书 / 企业微信等 Webhook 通知 + +以下示例使用 `node` 构建 JSON(自动转义特殊字符),`curl` 发送。通过 `env` 传入 `WEBHOOK_URL`: + +```bash +#!/bin/bash +WEBHOOK_URL="${WEBHOOK_URL:-}" + +STATUS="${STATUS:-completed}" +TITLE="${TITLE:-Untitled}" +DURATION="${DURATION:-0}" +BODY="${BODY:-(no output)}" + +PAYLOAD=$(node -e " +process.stdout.write(JSON.stringify({ + msg_type: 'interactive', + card: { + header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } }, + elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }] + } +})) +") + +curl -s -X POST "$WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" +``` + +```json +{ + "env": { + "WEBHOOK_URL": "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx" + }, + "notify": "/Users/you/.deepcode/notify-feishu.sh" +} +``` + +将 `WEBHOOK_URL` 替换为你的飞书机器人 Webhook 地址。此模式同样适用于 Slack、企业微信等 webhook 类通知,只需修改 JSON payload 格式。 + +## 终端通知(iTerm2 / Windows Terminal) + +如果你的终端是 iTerm2 或 Windows Terminal,可以直接通过 OSC 9 转义序列弹出终端原生通知,无需额外依赖。 + +创建 `~/.deepcode/notify.sh`: + +```bash +#!/bin/bash +# iTerm2 / Windows Terminal OSC 9 通知 +printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +Windows 用户如使用 Git Bash,上述脚本同样可用;也可创建 `.bat` 脚本: + +```batch +@echo off +REM Windows Terminal OSC 9 通知 +echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07 +``` + +## macOS 系统通知 + +```bash +#!/bin/bash +# macOS 系统通知 +osascript -e "display notification \"任务已${STATUS:-完成},耗时 ${DURATION}s\" with title \"DeepCode\"" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +## Linux 系统通知 + +需要安装 `libnotify-bin`: + +```bash +sudo apt install libnotify-bin # Debian/Ubuntu +``` + +创建 `~/.deepcode/notify.sh`: + +```bash +#!/bin/bash +# Linux notify-send 通知 +notify-send "DeepCode" "任务已${STATUS:-完成},耗时 ${DURATION}s" +``` + +```json +{ + "notify": "/home/you/.deepcode/notify.sh" +} +``` + +## Windows msg 弹窗通知 + +```batch +@echo off +REM Windows msg 弹窗通知 +msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)" +``` + +```json +{ + "notify": "C:\\Users\\you\\.deepcode\\notify.bat" +} +``` + +## 自定义通知脚本 + +你可以根据通知脚本注入的环境变量自行编写任意逻辑的通知脚本(Python、Node.js、Ruby 等均可),只要脚本可执行即可。脚本中可通过 `env` 字段传入额外需要的配置变量。 diff --git a/templates/skills/bundled/deepcode-self-refer/references/notify_en.md b/templates/skills/bundled/deepcode-self-refer/references/notify_en.md new file mode 100644 index 00000000..b949161c --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/notify_en.md @@ -0,0 +1,211 @@ +# Deep Code Task Completion Notification + +When the AI assistant finishes a round of tasks, Deep Code can automatically execute a notification script to send task results to your chosen channel (Slack, system notifications, etc.). + +## How It Works + +Configure the `notify` field in `settings.json` with the full path to an executable script. Every time the AI assistant completes a task response, Deep Code executes that script and injects context as environment variables. + +## Injected Environment Variables + +| Variable | Description | +|----------|-------------| +| `DURATION` | Session duration in seconds (integer) | +| `STATUS` | Session status: `"completed"` or `"failed"` | +| `FAIL_REASON` | Failure reason (only set on failure) | +| `BODY` | The text content of the last AI assistant reply | +| `TITLE` | Session title (matches the resume list title) | + +## Configuration + +Edit `~/.deepcode/settings.json` and add the `notify` field: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max", + "notify": "/path/to/your-notify-script.sh" +} +``` + +You can also configure custom environment variables for the notify script in `env`, such as a Slack Webhook URL: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-...", + "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/*****/****/**********" + }, + "notify": "/Users/you/.deepcode/notify-slack.sh" +} +``` + +These `env` variables are injected into the script's execution environment. + +## Slack Notification + +### 1. Get a Slack Webhook URL + +1. Create a [Slack App](https://api.slack.com/apps) +2. In the App page, go to **Incoming Webhooks** → **Add New Webhook to Workspace** to generate a Webhook URL + +### 2. Create the Notification Script + +Create `~/.deepcode/notify-slack.sh`: + +```bash +#!/usr/bin/env bash +SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}" +CURRENT_DIR=$(pwd) +BRANCH=$(git branch --show-current 2>/dev/null) +curl -X POST "$SLACK_WEBHOOK_URL" \ + -H "Content-type: application/json" \ + --data "{ + \"text\": \"✅ Deep Code task completed\n · cwd: $CURRENT_DIR\n · Branch: $BRANCH\n · Duration: $DURATION s\" + }" +``` + +Make the script executable: + +```bash +chmod +x ~/.deepcode/notify-slack.sh +``` + +### 3. Configure settings.json + +```json +{ + "env": { + "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/*****/****/**********" + }, + "notify": "/Users/you/.deepcode/notify-slack.sh" +} +``` + +> A Python version is also supported; you can pass and reference any custom environment variables via `env`. + +## Feishu / WeCom Webhook Notification + +Use `node` to build JSON (auto-escapes special characters) and `curl` to send. Pass `WEBHOOK_URL` via `env`: + +```bash +#!/bin/bash +WEBHOOK_URL="${WEBHOOK_URL:-}" + +STATUS="${STATUS:-completed}" +TITLE="${TITLE:-Untitled}" +DURATION="${DURATION:-0}" +BODY="${BODY:-(no output)}" + +PAYLOAD=$(node -e " +process.stdout.write(JSON.stringify({ + msg_type: 'interactive', + card: { + header: { title: { tag: 'plain_text', content: 'DeepCode: ' + process.env.TITLE + ' ' + process.env.STATUS + ' [' + process.env.DURATION + 's]' } }, + elements: [{ tag: 'markdown', content: (process.env.BODY || '').slice(0, 2000) || '(no output)' }] + } +})) +") + +curl -s -X POST "$WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" +``` + +```json +{ + "env": { + "WEBHOOK_URL": "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxx" + }, + "notify": "/Users/you/.deepcode/notify-feishu.sh" +} +``` + +Replace `WEBHOOK_URL` with your Feishu bot webhook URL. This pattern also works for other webhook-based notifications (Slack, WeCom, etc.) — just adjust the JSON payload format. + +## Terminal Notification (iTerm2 / Windows Terminal) + +On iTerm2 or Windows Terminal, you can use the OSC 9 escape sequence for native terminal notifications with zero dependencies. + +Create `~/.deepcode/notify.sh`: + +```bash +#!/bin/bash +# iTerm2 / Windows Terminal OSC 9 notification +printf '\x1b]9;DeepCode: task %s (%ss)\x07' "${STATUS:-completed}" "${DURATION}" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +Windows users on Git Bash can use the same script; alternatively, create a `.bat` script: + +```batch +@echo off +REM Windows Terminal OSC 9 notification +echo \x1b]9;DeepCode: task %STATUS% (%DURATION%s)\x07 +``` + +## macOS System Notification + +```bash +#!/bin/bash +# macOS system notification +osascript -e "display notification \"Task ${STATUS:-completed}, took ${DURATION}s\" with title \"DeepCode\"" +``` + +```json +{ + "notify": "/Users/you/.deepcode/notify.sh" +} +``` + +## Linux System Notification + +Requires `libnotify-bin`: + +```bash +sudo apt install libnotify-bin # Debian/Ubuntu +``` + +Create `~/.deepcode/notify.sh`: + +```bash +#!/bin/bash +# Linux notify-send notification +notify-send "DeepCode" "Task ${STATUS:-completed}, took ${DURATION}s" +``` + +```json +{ + "notify": "/home/you/.deepcode/notify.sh" +} +``` + +## Windows msg Popup Notification + +```batch +@echo off +REM Windows msg popup notification +msg %USERNAME% "DeepCode: task %STATUS% (%DURATION%s)" +``` + +```json +{ + "notify": "C:\\Users\\you\\.deepcode\\notify.bat" +} +``` + +## Custom Notification Scripts + +You can write your own notification scripts in any language (Python, Node.js, Ruby, etc.) using the injected environment variables and any additional variables passed via `env`. diff --git a/templates/skills/bundled/deepcode-self-refer/references/permission.md b/templates/skills/bundled/deepcode-self-refer/references/permission.md new file mode 100644 index 00000000..91c19c6f --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/permission.md @@ -0,0 +1,101 @@ +# Deep Code 权限机制 + +Deep Code 内置了一套细粒度的权限控制机制,在 AI 助手执行工具调用(如执行 Shell 命令、读写文件、访问网络等)前,根据用户配置的策略决定是自动放行、直接拒绝、还是弹出交互式确认。 + +## 概述 + +每次 AI 助手调用工具时,系统会自动分析该操作涉及的**权限范围(Permission Scope)**,然后根据 `settings.json` 中的权限配置做出决策。对于需要用户确认的操作,会在终端中弹出交互式选择界面,用户可以选择: + +- **Yes** — 仅本次放行 +- **Yes, and always allow** — 本次放行,并将该权限范围写入项目配置文件,后续同类操作不再询问 +- **No** — 拒绝本次操作 + +## 权限范围 + +Deep Code 定义了以下 10 种权限范围,覆盖了工具调用的各类风险场景: + +| 权限范围 | 说明 | +| -------- | ---- | +| `read-in-cwd` | 读取当前工作区内的文件 | +| `read-out-cwd` | 读取当前工作区外的文件 | +| `write-in-cwd` | 在当前工作区内创建或覆写文件 | +| `write-out-cwd` | 在当前工作区外创建或覆写文件 | +| `delete-in-cwd` | 删除当前工作区内的文件 | +| `delete-out-cwd` | 删除当前工作区外的文件 | +| `query-git-log` | 查询 Git 历史(如 `git log`、`git show`、`git blame`) | +| `mutate-git-log` | 修改 Git 历史(如 `git commit`、`git rebase`、`git tag`) | +| `network` | 访问网络(如 `curl`、`npm install` 等联网操作) | +| `mcp` | 调用 MCP 外部工具 | + +此外还有一个特殊的 `unknown` 范围,当 LLM 无法准确分类命令的副作用时使用,**`unknown` 总是触发询问**。 + +## 权限配置 + +在 `~/.deepcode/settings.json`(用户级)或 `.deepcode/settings.json`(项目级)中通过 `permissions` 字段配置: + +```json +{ + "permissions": { + "allow": [], + "deny": [], + "ask": [], + "defaultMode": "allowAll" + } +} +``` + +### 配置项说明 + +| 字段 | 类型 | 说明 | +| ---- | ---- | ---- | +| `allow` | `string[]` | 始终自动放行的权限范围列表 | +| `deny` | `string[]` | 始终自动拒绝的权限范围列表 | +| `ask` | `string[]` | 始终弹出询问的权限范围列表 | +| `defaultMode` | `"allowAll"` \| `"askAll"` | 未在 `allow`/`deny`/`ask` 中明确列出的权限范围的默认处理方式。默认为 `"allowAll"` | + +### 优先级规则 + +当一个工具调用涉及多个权限范围时,决策按以下优先级进行: + +1. 若任一范围命中 `deny` → **拒绝** +2. 若任一范围命中 `ask` → **询问** +3. 若所有范围均在 `allow` 中 → **自动放行** +4. 否则 → 按 `defaultMode` 处理 + +### 示例:宽松模式(默认) + +```json +{ + "permissions": { + "defaultMode": "allowAll" + } +} +``` + +默认行为:所有操作自动放行,无需确认。 + +### 示例:严格模式 + +```json +{ + "permissions": { + "allow": ["read-in-cwd", "write-in-cwd", "query-git-log"], + "defaultMode": "askAll" + } +} +``` + +此配置的效果: +- 工作区内读写、Git 查询 → 自动放行 +- 其他操作都需要用户确认。 + + +## 持久化机制 + +当用户在权限提示中选择 "Yes, and always allow" 后,对应的权限范围会被写入当前项目的 `.deepcode/settings.json` 文件中: + +- 新增范围会追加到 `permissions.allow` 列表 +- 如果该范围之前存在于 `deny` 或 `ask` 中,会被自动移除 +- 不会重复写入已存在的范围 + +这样后续同类操作就不再询问。 diff --git a/templates/skills/bundled/deepcode-self-refer/references/permission_en.md b/templates/skills/bundled/deepcode-self-refer/references/permission_en.md new file mode 100644 index 00000000..dae739c0 --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/permission_en.md @@ -0,0 +1,100 @@ +# Deep Code Permission Mechanism + +Deep Code includes a fine-grained permission control mechanism. Before the AI assistant executes a tool call (such as running a shell command, reading/writing files, accessing the network, etc.), the system determines whether to auto-allow, auto-deny, or prompt for interactive confirmation based on your configured policy. + +## Overview + +Each time the AI assistant invokes a tool, the system automatically analyzes the **permission scopes** involved and makes a decision based on the permission configuration in `settings.json`. For operations requiring user confirmation, an interactive prompt appears in the terminal with the following choices: + +- **Yes** — Allow this one time only +- **Yes, and always allow** — Allow this time and persistently save the scope to the project configuration so future calls skip the prompt +- **No** — Deny this operation + +## Permission Scopes + +Deep Code defines the following 10 permission scopes, covering various risk scenarios for tool calls: + +| Permission Scope | Description | +| ---------------- | ----------- | +| `read-in-cwd` | Read files inside the current workspace | +| `read-out-cwd` | Read files outside the current workspace | +| `write-in-cwd` | Create or overwrite files inside the current workspace | +| `write-out-cwd` | Create or overwrite files outside the current workspace | +| `delete-in-cwd` | Delete files inside the current workspace | +| `delete-out-cwd` | Delete files outside the current workspace | +| `query-git-log` | Query Git history (e.g., `git log`, `git show`, `git blame`) | +| `mutate-git-log` | Mutate Git history (e.g., `git commit`, `git rebase`, `git tag`) | +| `network` | Access the network (e.g., `curl`, `npm install`) | +| `mcp` | Invoke MCP external tools | + +There is also a special `unknown` scope used when the LLM cannot classify a command's side effects — **`unknown` always triggers a prompt**. + +## Permission Configuration + +Configure permissions in `~/.deepcode/settings.json` (user-level) or `.deepcode/settings.json` (project-level) via the `permissions` field: + +```json +{ + "permissions": { + "allow": [], + "deny": [], + "ask": [], + "defaultMode": "allowAll" + } +} +``` + +### Configuration Fields + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `allow` | `string[]` | Permission scopes that are always auto-allowed | +| `deny` | `string[]` | Permission scopes that are always auto-denied | +| `ask` | `string[]` | Permission scopes that always trigger a confirmation prompt | +| `defaultMode` | `"allowAll"` \| `"askAll"` | Default behavior for scopes not explicitly listed in `allow`/`deny`/`ask`. Defaults to `"allowAll"` | + +### Priority Rules + +When a tool call involves multiple permission scopes, the decision follows this priority: + +1. If any scope matches `deny` → **Deny** +2. If any scope matches `ask` → **Prompt** +3. If all scopes are in `allow` → **Auto-allow** +4. Otherwise → use `defaultMode` + +### Example: Relaxed Mode (default) + +```json +{ + "permissions": { + "defaultMode": "allowAll" + } +} +``` + +Default behavior: all operations are auto-allowed with no confirmation required. + +### Example: Strict Mode + +```json +{ + "permissions": { + "allow": ["read-in-cwd", "write-in-cwd", "query-git-log"], + "defaultMode": "askAll" + } +} +``` + +With this configuration: +- Reading/writing inside the workspace and querying Git history → auto-allowed +- All other operations → require user confirmation + +## Persistence + +When you select "Yes, and always allow" in a permission prompt, the corresponding scope is written to the project's `.deepcode/settings.json`: + +- The scope is appended to the `permissions.allow` list +- If the scope was previously in `deny` or `ask`, it is automatically removed +- Duplicate scopes are not written again + +This means subsequent calls involving the same scope will no longer prompt for confirmation. diff --git a/templates/skills/bundled/deepcode-self-refer/references/session-persistence.md b/templates/skills/bundled/deepcode-self-refer/references/session-persistence.md new file mode 100644 index 00000000..835d2881 --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/session-persistence.md @@ -0,0 +1,139 @@ +# 会话持久化机制 + +Deep Code 会把每个项目的会话记录保存在本机用户目录中。会话历史用于 `/resume`、`/continue` 和 `/undo`,不依赖当前终端进程是否仍在运行。 + +## 存储位置 + +每个项目都有独立的存储目录: + +```text +~/.deepcode/projects// +``` + +`` 由项目根目录路径生成。普通路径会转换为安全的目录名;路径过长时,Deep Code 会保留项目名的一部分,并追加稳定哈希,以避免存储路径过长。 + +项目存储目录包含以下主要文件和目录: + +| 路径 | 说明 | +| ---- | ---- | +| `sessions-index.json` | 当前项目的会话索引,保存会话列表和每个会话的概要信息。 | +| `.jsonl` | 单个会话的消息记录。每一行是一条 JSON 格式的消息。 | +| `file-history/.git` | 用于代码快照的内部 Git 仓库,供 `/undo` 恢复文件内容。 | + +## 持久化内容 + +### 会话索引 + +`sessions-index.json` 保存最近的会话条目。每个条目包含: + +- 会话 ID、标题、创建时间和更新时间。 +- 会话状态,例如 `pending`、`processing`、`completed`、`failed`、`interrupted`、`ask_permission`、`waiting_for_user`。 +- 最近一次 assistant 回复、思考内容、拒绝原因和失败原因。 +- 最近一次工具调用信息、token 用量和活跃 token 数。 +- 当前会话中仍被跟踪的子进程信息。 + +会话标题默认来自首次用户输入的前 100 个字符。使用会话列表中的重命名功能会更新索引里的标题。 + +### 消息文件 + +每个会话有一个独立的 JSONL 消息文件,文件名是 `.jsonl`。消息按追加顺序写入,常见字段包括: + +| 字段 | 说明 | +| ---- | ---- | +| `id` | 消息 ID。 | +| `sessionId` | 所属会话 ID。 | +| `role` | 消息角色:`system`、`user`、`assistant` 或 `tool`。 | +| `content` | 文本内容。 | +| `contentParams` | 结构化内容,例如图片输入。 | +| `messageParams` | 模型消息参数,例如 tool call ID、tool calls、reasoning content。 | +| `visible` | 是否在界面中显示。 | +| `compacted` | 是否已经被长会话压缩替代。 | +| `checkpointHash` | 与 `/undo` 关联的代码快照哈希。 | +| `meta` | 工具展示、skill、权限、摘要等附加信息。 | + +读取消息文件时,Deep Code 会逐行解析 JSON;无法解析的行会被忽略,以便尽量保留其余可用历史。 + +### 代码快照 + +Deep Code 使用 `file-history/.git` 保存代码快照。这个仓库只作为内部文件历史使用,不是项目仓库本身。 + +- 新会话会初始化一条以会话 ID 命名的内部分支。 +- 每次用户输入前,会记录已跟踪文件的状态。 +- 工具修改文件前后,会按需记录相关文件的状态。 +- 用户消息上的 `checkpointHash` 用来把某次对话位置和对应的代码状态关联起来。 + +快照只覆盖 Deep Code 已跟踪到的文件;无关文件不会因为 `/undo` 被任意改写。 + +## 会话生命周期 + +### 创建会话 + +创建新会话时,Deep Code 会: + +1. 生成新的会话 ID。 +2. 初始化该会话的代码快照分支。 +3. 在 `sessions-index.json` 中添加会话条目。 +4. 写入系统提示、运行时上下文、项目指令和用户消息。 +5. 启动模型请求,并在 assistant 回复和工具执行过程中持续更新索引和消息文件。 + +项目级会话列表最多保留最近 50 条记录。超过上限时,较旧会话会从索引中移除,其消息文件和相关运行时资源也会被清理。 + +### 继续会话 + +`/resume` 会显示当前项目的历史会话列表,并选择一个会话继续。 + +`/continue` 会优先继续当前活动会话;如果没有可继续的活动会话,则进入历史会话选择流程。 + +继续会话时,Deep Code 会读取会话消息文件,过滤已压缩的旧消息,修复未完成的工具调用上下文,并把可用历史转换为模型请求消息。 + +### 长会话压缩 + +当会话上下文过长时,Deep Code 会触发压缩流程: + +- 选取较早的一段非系统消息生成摘要。 +- 将这段旧消息标记为 `compacted: true`。 +- 在消息序列中插入一条不可见的系统摘要消息。 + +后续请求只会使用未压缩消息和摘要消息。原始消息仍保留在 JSONL 文件中,用于审计和界面历史展示。 + +### 中断、失败和权限等待 + +会话状态会随运行过程更新: + +- 用户中断后,状态会变为 `interrupted`,并清理当前会话控制器和被跟踪的子进程。 +- 请求失败时,状态会变为 `failed`,失败原因写入索引。 +- 工具调用需要确认时,状态会变为 `ask_permission`。 +- 工具需要用户输入时,状态会变为 `waiting_for_user`。 + +这些状态都会持久化到 `sessions-index.json`,因此重新打开 CLI 后仍能在会话列表中看到。 + +## `/undo` 如何使用持久化数据 + +`/undo` 的候选项来自可见且未压缩的用户消息。每个候选项会检查是否有关联的 `checkpointHash`,并确认对应快照是否可恢复。 + +根据选择,Deep Code 可以执行以下操作: + +| 操作 | 行为 | +| ---- | ---- | +| 恢复对话 | 截断所选用户消息之前的消息历史,并更新索引中的最新 assistant 信息。 | +| 恢复代码 | 从 `file-history/.git` 中读取所选快照,并还原被跟踪文件。 | +| 同时恢复 | 先恢复代码,再截断对话历史。 | + +恢复对话会重写该会话的 JSONL 文件;恢复代码会修改工作区中被快照跟踪的文件。 + +## 删除和重命名 + +在会话列表中删除会话会: + +- 从 `sessions-index.json` 移除该条目。 +- 删除对应的 `.jsonl` 文件。 +- 清理该会话的内存状态、临时工作目录状态、控制器和仍被跟踪的进程控制信息。 + +重命名会话只更新索引中的 `summary` 字段,不会改动消息文件或代码快照。 + +## 注意事项 + +- 会话数据保存在本机用户目录下,并按项目分隔。 +- 移动项目目录后,新的项目根路径会生成新的 ``;旧路径对应的历史不会自动迁移。 +- `file-history/.git` 是 Deep Code 的内部快照仓库,不应手动修改。 +- 会话删除不会清理内部 Git 仓库中的所有历史对象;它主要删除会话索引、消息文件和运行时资源。 diff --git a/templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md b/templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md new file mode 100644 index 00000000..071a5353 --- /dev/null +++ b/templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md @@ -0,0 +1,139 @@ +# Session Persistence + +Deep Code stores per-project session history in the local user directory. This history powers `/resume`, `/continue`, and `/undo`, and it remains available after the current terminal process exits. + +## Storage Location + +Each project has its own storage directory: + +```text +~/.deepcode/projects// +``` + +`` is generated from the project root path. Normal paths are converted into safe directory names. When the path would be too long, Deep Code keeps part of the project name and appends a stable hash so the storage path stays safe. + +The project storage directory contains these main files and directories: + +| Path | Description | +| ---- | ----------- | +| `sessions-index.json` | Session index for the current project, including the session list and summary metadata. | +| `.jsonl` | Message log for one session. Each line is one JSON message. | +| `file-history/.git` | Internal Git repository used for code checkpoints restored by `/undo`. | + +## Persisted Data + +### Session Index + +`sessions-index.json` stores recent session entries. Each entry includes: + +- Session ID, title, creation time, and update time. +- Session status, such as `pending`, `processing`, `completed`, `failed`, `interrupted`, `ask_permission`, or `waiting_for_user`. +- Latest assistant reply, thinking content, refusal reason, and failure reason. +- Latest tool-call data, token usage, and active token count. +- Metadata for subprocesses still tracked by the session. + +The default session title comes from the first 100 characters of the first user prompt. Renaming a session from the session list updates the title in the index. + +### Message Files + +Each session has a separate JSONL message file named `.jsonl`. Messages are appended in order. Common fields include: + +| Field | Description | +| ----- | ----------- | +| `id` | Message ID. | +| `sessionId` | Owning session ID. | +| `role` | Message role: `system`, `user`, `assistant`, or `tool`. | +| `content` | Text content. | +| `contentParams` | Structured content, such as image input. | +| `messageParams` | Model message parameters, such as tool call IDs, tool calls, and reasoning content. | +| `visible` | Whether the message is shown in the UI. | +| `compacted` | Whether the message has been replaced by long-session compaction. | +| `checkpointHash` | Code checkpoint hash associated with `/undo`. | +| `meta` | Extra metadata for tool display, skills, permissions, summaries, and related features. | + +When loading a message file, Deep Code parses JSON one line at a time. Malformed lines are ignored so the remaining usable history can still be loaded. + +### Code Checkpoints + +Deep Code stores code checkpoints in `file-history/.git`. This repository is only internal file history; it is not the project Git repository. + +- A new session initializes an internal branch named after the session ID. +- Before each user prompt, Deep Code records the state of files it already tracks. +- Before and after tool-based file mutations, Deep Code records the relevant file state as needed. +- `checkpointHash` on user messages links a conversation position to a code state. + +Checkpoints only cover files Deep Code has tracked. Unrelated files are not arbitrarily rewritten by `/undo`. + +## Session Lifecycle + +### Creating A Session + +When creating a new session, Deep Code: + +1. Generates a new session ID. +2. Initializes the code checkpoint branch for that session. +3. Adds an entry to `sessions-index.json`. +4. Writes system prompts, runtime context, project instructions, and the user message. +5. Starts the model request and keeps updating the index and message file as assistant replies and tool executions complete. + +The per-project session list keeps the 50 most recent entries. When the limit is exceeded, older sessions are removed from the index, and their message files and related runtime resources are cleaned up. + +### Continuing A Session + +`/resume` shows the current project's session history and lets you select a session to continue. + +`/continue` first continues the active session. If there is no active session to continue, it opens the session selection flow. + +When continuing a session, Deep Code reads the message file, filters compacted old messages, repairs incomplete tool-call context, and converts the usable history into model request messages. + +### Long-Session Compaction + +When the conversation context grows too large, Deep Code can compact earlier messages: + +- It summarizes an older range of non-system messages. +- It marks those old messages as `compacted: true`. +- It inserts an invisible system summary message into the message sequence. + +Future requests use the remaining active messages and the summary message. The original messages stay in the JSONL file for auditability and UI history. + +### Interruptions, Failures, And Permission Waits + +Session status changes during execution: + +- After a user interruption, status becomes `interrupted`, and Deep Code clears the current session controller and tracked subprocesses. +- After a request failure, status becomes `failed`, and the failure reason is written to the index. +- When a tool call needs confirmation, status becomes `ask_permission`. +- When a tool needs user input, status becomes `waiting_for_user`. + +These states are persisted in `sessions-index.json`, so they remain visible in the session list after reopening the CLI. + +## How `/undo` Uses Persistent Data + +`/undo` candidates come from visible, non-compacted user messages. Each candidate is checked for an associated `checkpointHash`, and Deep Code verifies whether the checkpoint can be restored. + +Depending on the selected mode, Deep Code can perform these operations: + +| Operation | Behavior | +| --------- | -------- | +| Restore conversation | Truncates message history before the selected user message and updates the latest assistant data in the index. | +| Restore code | Reads the selected checkpoint from `file-history/.git` and restores tracked files. | +| Restore both | Restores code first, then truncates the conversation history. | + +Restoring conversation rewrites the session JSONL file. Restoring code modifies workspace files tracked by the selected checkpoint. + +## Delete And Rename + +Deleting a session from the session list: + +- Removes the entry from `sessions-index.json`. +- Deletes the matching `.jsonl` file. +- Clears in-memory state, temporary working-directory state, controllers, and tracked process controls for that session. + +Renaming a session only updates the `summary` field in the index. It does not change message files or code checkpoints. + +## Notes + +- Session data is stored in the local user directory and separated by project. +- If a project directory is moved, the new project root path generates a new ``; history for the old path is not migrated automatically. +- `file-history/.git` is Deep Code's internal checkpoint repository and should not be edited manually. +- Deleting a session does not remove every historical object from the internal Git repository. It mainly removes the session index entry, message file, and runtime resources. diff --git a/templates/skills/bundled/skill-digester/SKILL.md b/templates/skills/bundled/skill-digester/SKILL.md new file mode 100644 index 00000000..6e9016d1 --- /dev/null +++ b/templates/skills/bundled/skill-digester/SKILL.md @@ -0,0 +1,115 @@ +--- +name: skill-digester +description: Reviews and improves another DeepCode skill's SKILL.md description field against the Agent Skills description-field rules. Use when the user asks to "digest" a skill, including requests like "digest the pdf skill" or "消化 pdf 技能". +--- + +# Skill Digester + +Use this skill to review and optionally rewrite the `description` field of another DeepCode skill. + +## Interaction Rule + +Whenever user input is needed, call the `AskUserQuestion` tool. Do not ask follow-up questions as plain assistant text. This includes missing skill names, language preference, duplicate matches, malformed frontmatter decisions, and whether to apply a recommended rewrite. + +## Workflow + +1. Identify the target skill from the user's request. + - If the user did not provide a skill name, use `AskUserQuestion` to ask for one. + - Locate the skill by running the bundled Node script from this skill directory: + + ```bash + node ~/.deepcode/skills/skill-digester/scripts/find-skill.js "" "" + ``` + + If this skill is loaded from a project-level or different user-level path, use the `scripts/find-skill.js` file next to this `SKILL.md` instead. + - The script searches the same roots Deep Code CLI scans, in priority order: + 1. Project native skills: `./.deepcode/skills//SKILL.md` + 2. Project interoperable skills: `./.agents/skills//SKILL.md` + 3. User native skills: `~/.deepcode/skills//SKILL.md` + 4. User interoperable skills: `~/.agents/skills//SKILL.md` + - Treat `./` as the current Deep Code project root only; do not scan parent directories unless the running project root is changed. + - The script resolves each candidate's skill name the way Deep Code does: use the trimmed frontmatter `name` when present, otherwise use the folder name with underscores converted to hyphens. + - Match the user's input against the resolved skill name first. If needed, also consider the folder name or an explicit path the user provided. + - Treat the matched skill's `path` as the source `SKILL.md` to review. + - Treat the matched skill's `digestTarget.path` as the only output `SKILL.md` path to create or edit. + - `digestTarget.path` always points to the same scope's native Deep Code root: + - Project sources from `./.deepcode/skills` or `./.agents/skills` digest to `./.deepcode/skills//SKILL.md`. + - User sources from `~/.deepcode/skills` or `~/.agents/skills` digest to `~/.deepcode/skills//SKILL.md`. + - If the script returns one active match, use its `path` for reading and `digestTarget.path` for writing. + - If the script returns active and shadowed matches, present each source path and digest target path, then use `AskUserQuestion` before using a shadowed source. + - If the script returns no match, state that the skill was not found in Deep Code's scanned skill roots and use `AskUserQuestion` to ask whether the user wants to try another name. + +2. Infer the user's preferred language before reviewing. + - Infer a likely language from the user's wording. For example, if the user says `消化pdf技能`, infer Chinese. + - Confirm the language with `AskUserQuestion` in the inferred language. For Chinese, ask: `请选择您偏好的语言。` + - Offer the inferred language first and include `English` as a fallback. The UI provides an `Other` option, so the user can type a different language. + - Use the confirmed preferred language for every later question, recommendation, and rewritten `description` field. + +3. Read the source `SKILL.md`. + - Parse the YAML frontmatter and Markdown body from the matched source path. + - Preserve all frontmatter fields and body content except for the `description` field if the user approves a rewrite. + - If frontmatter is missing or malformed, explain the issue and use `AskUserQuestion` before making structural repairs. + +4. Review the current `description` field against the Agent Skills specification. + - Required constraints: + - It must be non-empty. + - It must be 1-1024 characters. + - It should describe what the skill does. + - It should describe when to use the skill. + - It should include specific keywords that help agents identify relevant tasks. + - Compare the description with the actual `SKILL.md` body. Flag mismatches, missing capabilities, overbroad activation language, vague wording, or important trigger keywords that are absent. + - Do not rewrite for style alone if the existing description is accurate, specific, and useful. + +5. Present the review and recommendation. + - If the description is already good, say so and do not change the file unless the user asks. + - If improvements are useful, show: + - The current description. + - Concise review findings. + - A recommended replacement written in the preferred language. + - The source path being reviewed. + - The digest output path that would be created or edited. + - Use `AskUserQuestion` to ask the user to choose one of three actions in the preferred language: + - Apply the recommended change. + - Abandon the change. + - Continue discussing the wording. + +6. Apply the change only after explicit approval. + - Write only to `digestTarget.path`; never write the digested result to `.agents/skills`. + - If `digestTarget.sameAsSource` is true, update only the `description` field in that existing native `SKILL.md`. + - If `digestTarget.sameAsSource` is false and `digestTarget.exists` is false, create the native target skill directory by copying the source skill directory first, then update only the target `SKILL.md` description. This preserves bundled scripts, references, and assets. + - If `digestTarget.sameAsSource` is false and `digestTarget.exists` is true, update only the `description` field in the existing native target `SKILL.md`; do not overwrite its body or bundled files unless the user explicitly asks. + - Keep the original `name` and any other frontmatter fields unchanged in the file being written. + - Preserve body content exactly unless the user separately asks to edit it. + - After editing, report the source path, updated digest output path, and final description. + +## AskUserQuestion Patterns + +Use one question at a time unless two decisions are tightly coupled. Each question must include `options`; rely on the UI's `Other` option for free-form input. + +Examples: + +```json +{"questions":[{"question":"请选择您偏好的语言。","options":[{"label":"中文","description":"后续询问和推荐描述都使用中文。"},{"label":"English","description":"Use English for follow-up questions and the recommended description."}]}]} +``` + +```json +{"questions":[{"question":"How should I proceed with this description recommendation?","options":[{"label":"Apply change","description":"Update only the description field in the native digest output SKILL.md."},{"label":"Abandon change","description":"Leave the file unchanged."},{"label":"Discuss wording","description":"Continue refining the proposed description before editing."}]}]} +``` + +## Review Heuristics + +A strong description is short, concrete, and activation-oriented. Prefer this pattern: + +```text +. Use when . +``` + +Avoid descriptions that are only generic labels, marketing copy, or internal implementation notes. + +## Safety Notes + +- Never modify a different skill with a similar name without asking. +- Never save the digested output under `.agents/skills`; `.agents/skills` is only a source root for digestion. +- Never move a skill between project and user level during digestion. +- Never change the target skill's language preference after confirmation unless the user asks. + diff --git a/templates/skills/bundled/skill-digester/scripts/find-skill.js b/templates/skills/bundled/skill-digester/scripts/find-skill.js new file mode 100755 index 00000000..2067e023 --- /dev/null +++ b/templates/skills/bundled/skill-digester/scripts/find-skill.js @@ -0,0 +1,215 @@ +#!/usr/bin/env node +/* global __dirname, console, process, require */ + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +function usage() { + return "Usage: node scripts/find-skill.js [project-root]"; +} + +function loadMatter() { + for (const base of [process.cwd(), __dirname]) { + try { + const resolved = require.resolve("gray-matter", { paths: [base] }); + return require(resolved); + } catch { + // Try the next lookup base, then fall back to the local parser. + } + } + return null; +} + +function parseFrontmatter(content) { + const matter = loadMatter(); + if (matter) { + try { + return matter(content).data || {}; + } catch { + // Fall back to the minimal frontmatter parser below. + } + } + + if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) { + return {}; + } + const newline = content.startsWith("---\r\n") ? "\r\n" : "\n"; + const end = content.indexOf(`${newline}---${newline}`, 4); + if (end === -1) { + return {}; + } + const raw = content.slice(4, end).split(/\r?\n/); + const data = {}; + for (const line of raw) { + const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (!match) continue; + let value = match[2].trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + data[match[1]] = value; + } + return data; +} + +function readSkillInfo(skillPath, displayPath, folderName) { + const fallbackName = folderName.replace(/_/g, "-"); + try { + const content = fs.readFileSync(skillPath, "utf8"); + const data = parseFrontmatter(content); + const name = typeof data.name === "string" && data.name.trim() ? data.name.trim() : fallbackName; + const description = typeof data.description === "string" ? data.description.trim() : ""; + return { name, folderName, path: skillPath, displayPath, description }; + } catch (error) { + return { name: fallbackName, folderName, path: skillPath, displayPath, description: "", error: error.message }; + } +} + +function isSkillFile(candidatePath) { + try { + return fs.statSync(candidatePath).isFile(); + } catch { + return false; + } +} + +function collect(rootInfo) { + let entries; + try { + entries = fs.readdirSync(rootInfo.root, { withFileTypes: true }); + } catch { + return []; + } + + const skills = []; + for (const entry of entries) { + if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; + const folderName = entry.name; + const skillPath = path.join(rootInfo.root, folderName, "SKILL.md"); + if (!isSkillFile(skillPath)) continue; + const skill = readSkillInfo(skillPath, `${rootInfo.displayRoot}/${folderName}/SKILL.md`, folderName); + const digestTargetPath = path.join(rootInfo.digestRoot, folderName, "SKILL.md"); + skill.digestTarget = { + path: digestTargetPath, + displayPath: `${rootInfo.digestDisplayRoot}/${folderName}/SKILL.md`, + root: rootInfo.digestDisplayRoot, + exists: isSkillFile(digestTargetPath), + sameAsSource: path.resolve(digestTargetPath) === path.resolve(skillPath), + }; + skills.push(skill); + } + return skills; +} + +function expandInputPath(input, projectRoot) { + if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2)); + if (input.startsWith("~\\")) return path.join(os.homedir(), input.slice(2)); + if (input.startsWith("./")) return path.join(projectRoot, input.slice(2)); + if (input.startsWith(".\\")) return path.join(projectRoot, input.slice(2)); + if (path.isAbsolute(input)) return input; + return null; +} + +function main() { + const query = process.argv[2]; + const projectRoot = process.argv[3] ? path.resolve(process.argv[3]) : process.cwd(); + if (!query) { + console.error(usage()); + process.exit(2); + } + + const projectNativeRoot = path.join(projectRoot, ".deepcode", "skills"); + const userNativeRoot = path.join(os.homedir(), ".deepcode", "skills"); + const roots = [ + { + root: projectNativeRoot, + displayRoot: "./.deepcode/skills", + scope: "project", + kind: "native", + digestRoot: projectNativeRoot, + digestDisplayRoot: "./.deepcode/skills", + }, + { + root: path.join(projectRoot, ".agents", "skills"), + displayRoot: "./.agents/skills", + scope: "project", + kind: "interoperable", + digestRoot: projectNativeRoot, + digestDisplayRoot: "./.deepcode/skills", + }, + { + root: userNativeRoot, + displayRoot: "~/.deepcode/skills", + scope: "user", + kind: "native", + digestRoot: userNativeRoot, + digestDisplayRoot: "~/.deepcode/skills", + }, + { + root: path.join(os.homedir(), ".agents", "skills"), + displayRoot: "~/.agents/skills", + scope: "user", + kind: "interoperable", + digestRoot: userNativeRoot, + digestDisplayRoot: "~/.deepcode/skills", + }, + ]; + + const scanned = []; + for (const rootInfo of roots) { + for (const skill of collect(rootInfo)) { + scanned.push({ ...skill, root: rootInfo.displayRoot, scope: rootInfo.scope, kind: rootInfo.kind }); + } + } + + const activeByName = new Map(); + const shadowed = []; + for (const skill of scanned) { + if (activeByName.has(skill.name)) { + shadowed.push({ ...skill, shadowedBy: activeByName.get(skill.name).displayPath }); + } else { + activeByName.set(skill.name, skill); + } + } + + const inputPath = expandInputPath(query, projectRoot); + const matches = []; + for (const skill of scanned) { + if (skill.name === query || skill.folderName === query) { + matches.push(skill); + continue; + } + if (inputPath) { + const normalized = path.resolve(inputPath); + if (path.resolve(skill.path) === normalized || path.resolve(path.dirname(skill.path)) === normalized) { + matches.push(skill); + } + } + } + + const activeMatches = matches.filter((skill) => activeByName.get(skill.name)?.path === skill.path); + const shadowedMatches = matches.filter((skill) => activeByName.get(skill.name)?.path !== skill.path); + + process.stdout.write( + JSON.stringify( + { + query, + projectRoot, + roots, + found: matches.length > 0, + activeMatches, + shadowedMatches: shadowedMatches.map((skill) => ({ + ...skill, + shadowedBy: activeByName.get(skill.name)?.displayPath, + })), + duplicateNames: shadowed, + }, + null, + 2 + ) + ); + process.stdout.write("\n"); +} + +main(); diff --git a/templates/skills/bundled/skill-writer/SKILL.md b/templates/skills/bundled/skill-writer/SKILL.md new file mode 100644 index 00000000..e30bfdfc --- /dev/null +++ b/templates/skills/bundled/skill-writer/SKILL.md @@ -0,0 +1,381 @@ +--- +name: skill-writer +description: Guide users through creating, updating, debugging, and validating Agent Skills for AI agents. Use when the user wants to create, write, author, design, troubleshoot, validate, or improve a Skill, or needs help with SKILL.md files, frontmatter, or skill structure. +--- + +# Skill Writer + +This Skill helps you create well-structured Agent Skills for AI agents that follow best practices and validation requirements. + +## When to use this Skill + +Use this Skill when: +- Creating a new Agent Skill +- Writing or updating SKILL.md files +- Designing skill structure and frontmatter +- Troubleshooting skill discovery issues +- Converting existing prompts or workflows into Skills + +## Instructions + +### Step 1: Determine Skill scope + +First, understand what the Skill should do: + +1. **Ask clarifying questions**: + - What specific capability should this Skill provide? + - When should AI agents use this Skill? + - What tools or resources does it need? + - Is this for personal use or team sharing? + +2. **Keep it focused**: One Skill = one capability + - Good: "PDF form filling", "Excel data analysis" + - Too broad: "Document processing", "Data tools" + +### Step 2: Choose Skill location + +Determine where to create the Skill: + +**Personal Skills** (`~/.agents/skills/`): +- Individual workflows and preferences +- Experimental Skills +- Personal productivity tools + +**Project Skills** (`.agents/skills/`): +- Team workflows and conventions +- Project-specific expertise +- Shared utilities (committed to git) + +### Step 3: Create Skill structure + +Create the directory and files: + +```bash +# Personal +mkdir -p ~/.agents/skills/skill-name + +# Project +mkdir -p .agents/skills/skill-name +``` + +For multi-file Skills: +``` +skill-name/ +├── SKILL.md (required) +├── reference.md (optional) +├── examples.md (optional) +├── scripts/ +│ └── helper.py (optional) +└── templates/ + └── template.txt (optional) +``` + +### Step 4: Write SKILL.md frontmatter + +Create YAML frontmatter with required fields: + +```yaml +--- +name: skill-name +description: Brief description of what this does and when to use it +--- +``` + +**Field requirements**: + +- **name**: + - Lowercase letters, numbers, hyphens only + - Max 64 characters + - Must match directory name + - Good: `pdf-processor`, `git-commit-helper` + - Bad: `PDF_Processor`, `Git Commits!` + +- **description**: + - Max 1024 characters + - Include BOTH what it does AND when to use it + - Use specific trigger words users would say + - Mention file types, operations, and context + +**Optional frontmatter fields**: + +- **allowed-tools**: Restrict tool access (comma-separated list) + ```yaml + allowed-tools: read + ``` + Use for: + - Read-only Skills + - Security-sensitive workflows + - Limited-scope operations + +### Step 5: Write effective descriptions + +The description is critical for AI agents to discover your Skill. + +**Formula**: `[What it does] + [When to use it] + [Key triggers]` + +**Examples**: + +✅ **Good**: +```yaml +description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction. +``` + +✅ **Good**: +```yaml +description: Analyze Excel spreadsheets, create pivot tables, and generate charts. Use when working with Excel files, spreadsheets, or analyzing tabular data in .xlsx format. +``` + +❌ **Too vague**: +```yaml +description: Helps with documents +description: For data analysis +``` + +**Tips**: +- Include specific file extensions (.pdf, .xlsx, .json) +- Mention common user phrases ("analyze", "extract", "generate") +- List concrete operations (not generic verbs) +- Add context clues ("Use when...", "For...") + +### Step 6: Structure the Skill content + +Use clear Markdown sections: + +```markdown +# Skill Name + +Brief overview of what this Skill does. + +## Quick start + +Provide a simple example to get started immediately. + +## Instructions + +Step-by-step guidance for AI agents: +1. First step with clear action +2. Second step with expected outcome +3. Handle edge cases + +## Examples + +Show concrete usage examples with code or commands. + +## Best practices + +- Key conventions to follow +- Common pitfalls to avoid +- When to use vs. not use + +## Requirements + +List any dependencies or prerequisites: +```bash +pip install package-name +``` + +## Advanced usage + +For complex scenarios, see [reference.md](reference.md). +``` + +### Step 7: Add supporting files (optional) + +Create additional files for progressive disclosure: + +**reference.md**: Detailed API docs, advanced options +**examples.md**: Extended examples and use cases +**scripts/**: Helper scripts and utilities +**templates/**: File templates or boilerplate + +Reference them from SKILL.md: +```markdown +For advanced usage, see [reference.md](reference.md). + +Run the helper script: +\`\`\`bash +python scripts/helper.py input.txt +\`\`\` +``` + +### Step 8: Validate the Skill + +Check these requirements: + +✅ **File structure**: +- [ ] SKILL.md exists in correct location +- [ ] Directory name matches frontmatter `name` + +✅ **YAML frontmatter**: +- [ ] Opening `---` on line 1 +- [ ] Closing `---` before content +- [ ] Valid YAML (no tabs, correct indentation) +- [ ] `name` follows naming rules +- [ ] `description` is specific and < 1024 chars + +✅ **Content quality**: +- [ ] Clear instructions for AI agents +- [ ] Concrete examples provided +- [ ] Edge cases handled +- [ ] Dependencies listed (if any) + +✅ **Testing**: +- [ ] Description matches user questions +- [ ] Skill activates on relevant queries +- [ ] Instructions are clear and actionable + +### Step 9: Test the Skill + +1. **Restart AI agents** (if running) to load the Skill + +2. **Ask relevant questions** that match the description: + ``` + Can you help me extract text from this PDF? + ``` + +3. **Verify activation**: AI agents should use the Skill automatically + +4. **Check behavior**: Confirm AI agents follows the instructions correctly + +### Step 10: Debug if needed + +If AI agents doesn't use the Skill: + +1. **Make description more specific**: + - Add trigger words + - Include file types + - Mention common user phrases + +2. **Check file location**: + ```bash + ls ~/.agents/skills/skill-name/SKILL.md + ls .agents/skills/skill-name/SKILL.md + ``` + +3. **Validate YAML**: + ```bash + cat SKILL.md | head -n 10 + ``` + +## Common patterns + +### Read-only Skill + +```yaml +--- +name: code-reader +description: Read and analyze code without making changes. Use for code review, understanding codebases, or documentation. +allowed-tools: read +--- +``` + +### Script-based Skill + +```yaml +--- +name: data-processor +description: Process CSV and JSON data files with Python scripts. Use when analyzing data files or transforming datasets. +--- + +# Data Processor + +## Instructions + +1. Use the processing script: +\`\`\`bash +python scripts/process.py input.csv --output results.json +\`\`\` + +2. Validate output with: +\`\`\`bash +python scripts/validate.py results.json +\`\`\` +``` + +### Multi-file Skill with progressive disclosure + +```yaml +--- +name: api-designer +description: Design REST APIs following best practices. Use when creating API endpoints, designing routes, or planning API architecture. +--- + +# API Designer + +Quick start: See [examples.md](examples.md) + +Detailed reference: See [reference.md](reference.md) + +## Instructions + +1. Gather requirements +2. Design endpoints (see examples.md) +3. Document with OpenAPI spec +4. Review against best practices (see reference.md) +``` + +## Best practices for Skill authors + +1. **One Skill, one purpose**: Don't create mega-Skills +2. **Specific descriptions**: Include trigger words users will say +3. **Clear instructions**: Write for AI agents, not humans +4. **Concrete examples**: Show real code, not pseudocode +5. **List dependencies**: Mention required packages in description +6. **Test with teammates**: Verify activation and clarity +7. **Version your Skills**: Document changes in content +8. **Use progressive disclosure**: Put advanced details in separate files + +## Validation checklist + +Before finalizing a Skill, verify: + +- [ ] Name is lowercase, hyphens only, max 64 chars +- [ ] Description is specific and < 1024 chars +- [ ] Description includes "what" and "when" +- [ ] YAML frontmatter is valid +- [ ] Instructions are step-by-step +- [ ] Examples are concrete and realistic +- [ ] Dependencies are documented +- [ ] File paths use forward slashes +- [ ] Skill activates on relevant queries +- [ ] AI agents follows instructions correctly + +## Troubleshooting + +**Skill doesn't activate**: +- Make description more specific with trigger words +- Include file types and operations in description +- Add "Use when..." clause with user phrases + +**Multiple Skills conflict**: +- Make descriptions more distinct +- Use different trigger words +- Narrow the scope of each Skill + +**Skill has errors**: +- Check YAML syntax (no tabs, proper indentation) +- Verify file paths (use forward slashes) +- Ensure scripts have execute permissions +- List all dependencies + +## Examples + +See the documentation for complete examples: +- Simple single-file Skill (commit-helper) +- Skill with tool permissions (code-reviewer) +- Multi-file Skill (pdf-processing) + +## Output format + +When creating a Skill, I will: + +1. Ask clarifying questions about scope and requirements +2. Suggest a Skill name and location +3. Create the SKILL.md file with proper frontmatter +4. Include clear instructions and examples +5. Add supporting files if needed +6. Provide testing instructions +7. Validate against all requirements + +The result will be a complete, working Skill that follows all best practices and validation rules. + From 84e174de75019be2f2d3061408e63581c69df4ba Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 9 Jun 2026 15:36:53 +0800 Subject: [PATCH 147/212] feat: add raw mode shortcut `ctrl+r` --- src/tests/prompt-input-keys.test.ts | 8 ++++++++ src/tests/welcome-screen.test.ts | 1 + src/ui/index.ts | 1 + src/ui/views/PromptInput.tsx | 19 +++++++++++++++++-- src/ui/views/WelcomeScreen.tsx | 1 + 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/tests/prompt-input-keys.test.ts b/src/tests/prompt-input-keys.test.ts index a8999b6b..bcad3395 100644 --- a/src/tests/prompt-input-keys.test.ts +++ b/src/tests/prompt-input-keys.test.ts @@ -15,6 +15,7 @@ import { getPromptReturnKeyAction, isPromptCursorAtWrapBoundary, isClearImageAttachmentsShortcut, + isRawModeShortcut, removeCurrentSlashToken, resolvePromptTerminalCursorPosition, toggleSkillSelection, @@ -206,6 +207,13 @@ test("parseTerminalInput recognizes ctrl+x as the image attachment clear shortcu assert.equal(isClearImageAttachmentsShortcut(input, key), true); }); +test("parseTerminalInput recognizes ctrl+r as the raw mode shortcut", () => { + const { input, key } = parseTerminalInput("\u0012"); + assert.equal(input, "r"); + assert.equal(key.ctrl, true); + assert.equal(isRawModeShortcut(input, key), true); +}); + test("parseTerminalInput recognizes ctrl+- modifyOtherKeys sequence (standard)", () => { const { input, key } = parseTerminalInput("\u001B[45;5u"); assert.equal(input, "-"); diff --git a/src/tests/welcome-screen.test.ts b/src/tests/welcome-screen.test.ts index df7e109b..45bb7413 100644 --- a/src/tests/welcome-screen.test.ts +++ b/src/tests/welcome-screen.test.ts @@ -32,5 +32,6 @@ test("buildWelcomeTips includes built-in slash commands and loaded skills", () = const labels = tips.map((tip) => tip.label); assert.ok(labels.includes("/new")); assert.ok(labels.includes("/loaded")); + assert.ok(labels.includes("Ctrl+R")); assert.equal(labels.includes("/fresh"), false); }); diff --git a/src/ui/index.ts b/src/ui/index.ts index 2504bbd8..8f155360 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -26,6 +26,7 @@ export { toggleSkillSelection, removeCurrentSlashToken, isClearImageAttachmentsShortcut, + isRawModeShortcut, getPromptReturnKeyAction, renderBufferWithCursor, buildInitPromptSubmission, diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index c81d2237..c6b150cb 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -169,18 +169,19 @@ export const PromptInput = React.memo(function PromptInput({ const showFileMentionMenu = !showSkillsDropdown && !showModelDropdown && + !openRawModelDropdown && fileMentionToken !== null && fileMentionKey !== dismissedFileMentionKey; const slashItems = React.useMemo(() => buildSlashCommands(skills), [skills]); const slashToken = getCurrentSlashToken(buffer); const slashMenu = React.useMemo( () => - showSkillsDropdown || showModelDropdown || showFileMentionMenu + showSkillsDropdown || showModelDropdown || openRawModelDropdown || showFileMentionMenu ? [] : slashToken ? filterSlashCommands(slashItems, slashToken) : [], - [showSkillsDropdown, showModelDropdown, showFileMentionMenu, slashToken, slashItems] + [showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu, slashToken, slashItems] ); const showMenu = slashMenu.length > 0; const promptHistoryKey = React.useMemo(() => promptHistory.join("\0"), [promptHistory]); @@ -315,6 +316,9 @@ export const PromptInput = React.memo(function PromptInput({ } if (key.escape) { + if (openRawModelDropdown) { + return; + } if (showFileMentionMenu) { return; } @@ -325,6 +329,13 @@ export const PromptInput = React.memo(function PromptInput({ return; } + if (isRawModeShortcut(input, key)) { + setShowSkillsDropdown(false); + setShowModelDropdown(false); + setOpenRawModelDropdown(true); + return; + } + if (key.ctrl && (input === "o" || input === "O")) { if (runningProcesses && runningProcesses.size > 0 && onToggleProcessStdout) { onToggleProcessStdout(); @@ -887,6 +898,10 @@ export function isClearImageAttachmentsShortcut(input: string, key: Pick): boolean { + return key.ctrl && (input === "r" || input === "R"); +} + export type PromptReturnKeyAction = "submit" | "newline" | null; export function getPromptReturnKeyAction(key: Pick): PromptReturnKeyAction { diff --git a/src/ui/views/WelcomeScreen.tsx b/src/ui/views/WelcomeScreen.tsx index 96aef71f..bee7e9ae 100644 --- a/src/ui/views/WelcomeScreen.tsx +++ b/src/ui/views/WelcomeScreen.tsx @@ -23,6 +23,7 @@ const SHORTCUT_TIPS = [ { label: "Enter", description: "Send the prompt" }, { label: "Shift+Enter", description: "Insert a newline" }, { label: "Ctrl+V", description: "Paste an image from the clipboard" }, + { label: "Ctrl+R", description: "Open raw display mode selection" }, { label: "Esc", description: "Interrupt the current model turn" }, { label: "/", description: "Open the skills and commands menu" }, { label: "Ctrl+D twice", description: "Quit Deep Code CLI" }, From 9dca45905eab098c006099248853bdd75d94117a Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 9 Jun 2026 15:37:28 +0800 Subject: [PATCH 148/212] 0.1.29 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 05d7c4eb..09db34ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.28", + "version": "0.1.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.28", + "version": "0.1.29", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index 911bd74e..aa925b92 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.28", + "version": "0.1.29", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From bd6461908e38c6270c77c50960c7ccc45f270590 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 10 Jun 2026 08:59:12 +0800 Subject: [PATCH 149/212] feat: enhance Windows MCP command quoting and add tests for cmd metacharacters --- src/mcp/mcp-client.ts | 4 +- src/tests/mcp-client.test.ts | 78 +++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index d2ef1c88..4420c569 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -427,7 +427,7 @@ export function createMcpSpawnSpec( // On Windows, shell: true lets cmd.exe resolve the command via PATHEXT // (npx -> npx.cmd, etc.). Join command and args into a single string // with empty spawn args to avoid Node 24 DEP0190. - // Only quote arguments that contain spaces or double-quotes to prevent + // Only quote arguments that need protection from cmd.exe to prevent // double-wrapping by Node.js's own shell quoting. command: [command, ...args].map(quoteWindowsArgIfNeeded).join(" "), args: [], @@ -444,7 +444,7 @@ export function createMcpSpawnSpec( } function quoteWindowsArgIfNeeded(arg: string): string { - if (arg.includes(" ") || arg.includes('"')) { + if (/[\s"&|<>^()]/.test(arg)) { return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, "$&$&")}"`; } return arg; diff --git a/src/tests/mcp-client.test.ts b/src/tests/mcp-client.test.ts index 29151d3a..6a7dc016 100644 --- a/src/tests/mcp-client.test.ts +++ b/src/tests/mcp-client.test.ts @@ -1,6 +1,9 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { createMcpSpawnSpec } from "../mcp/mcp-client"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { McpClient, createMcpSpawnSpec } from "../mcp/mcp-client"; test("createMcpSpawnSpec keeps non-Windows MCP launches shell-free", () => { assert.deepEqual(createMcpSpawnSpec("npx", ["-y", "@playwright/mcp@latest"], "darwin"), { @@ -32,3 +35,76 @@ test("createMcpSpawnSpec quotes Windows command paths and arguments", () => { ); assert.deepEqual(spec.args, []); }); + +test("createMcpSpawnSpec quotes Windows args with cmd metacharacters", () => { + const spec = createMcpSpawnSpec( + "npx", + [ + "-y", + "some-mcp", + "--url=https://example.test?a=1&b=2", + "--pipe=a|b", + "--redirect=out", + "--caret=^value", + "--group=(value)", + ], + "win32" + ); + + assert.equal( + spec.command, + [ + "npx", + "-y", + "some-mcp", + '"--url=https://example.test?a=1&b=2"', + '"--pipe=a|b"', + '"--redirect=out"', + '"--caret=^value"', + '"--group=(value)"', + ].join(" ") + ); + assert.deepEqual(spec.args, []); +}); + +test("McpClient starts a PATH-resolved cmd MCP server on Windows", { skip: process.platform !== "win32" }, async () => { + const serverDir = mkdtempSync(path.join(tmpdir(), "deepcode-mcp-probe-")); + const originalPath = process.env.PATH; + + writeFileSync(path.join(serverDir, "mcp-probe.cmd"), '@echo off\r\nnode "%~dp0mcp-probe-server.cjs"\r\n'); + writeFileSync( + path.join(serverDir, "mcp-probe-server.cjs"), + [ + 'const readline = require("node:readline");', + "const rl = readline.createInterface({ input: process.stdin });", + "function send(message) { process.stdout.write(`${JSON.stringify(message)}\\n`); }", + 'rl.on("line", (line) => {', + " const request = JSON.parse(line);", + ' if (request.method === "initialize") {', + ' send({ jsonrpc: "2.0", id: request.id, result: { protocolVersion: "2025-03-26", capabilities: {}, serverInfo: { name: "probe", version: "1.0.0" } } });', + " return;", + " }", + ' if (request.method === "tools/list") {', + ' send({ jsonrpc: "2.0", id: request.id, result: { tools: [{ name: "probe_tool", inputSchema: { type: "object", properties: {} } }] } });', + " return;", + " }", + "});", + ].join("\n") + ); + + process.env.PATH = `${serverDir}${path.delimiter}${originalPath ?? ""}`; + const client = new McpClient("probe", "mcp-probe", []); + + try { + await client.connect(5_000); + const tools = await client.listTools(5_000); + assert.deepEqual( + tools.map((tool) => tool.name), + ["probe_tool"] + ); + } finally { + client.disconnect(); + process.env.PATH = originalPath; + rmSync(serverDir, { recursive: true, force: true }); + } +}); From 29adf6ab7406e54de97dc52f34b8cbe9681c1119 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 11 Jun 2026 20:46:20 +0800 Subject: [PATCH 150/212] feat: add plan mode skill and enhance shell init command tests --- src/common/shell-utils.ts | 9 +- src/session.ts | 56 +++----- src/tests/message-view.test.ts | 8 +- src/tests/session.test.ts | 86 ++++++++++- src/tests/shell-utils.test.ts | 13 ++ templates/skills/bundled/plan/SKILL.md | 133 ++++++++++++++++++ .../skills/bundled/skill-writer/SKILL.md | 2 +- 7 files changed, 266 insertions(+), 41 deletions(-) create mode 100644 templates/skills/bundled/plan/SKILL.md diff --git a/src/common/shell-utils.ts b/src/common/shell-utils.ts index 1dcec25a..eb8a2da7 100644 --- a/src/common/shell-utils.ts +++ b/src/common/shell-utils.ts @@ -83,9 +83,14 @@ export function getShellKind(shellPath: string): ShellKind { export function buildShellInitCommand(shellPath: string): string | null { switch (getShellKind(shellPath)) { case "zsh": - return ['ZSHRC="${ZDOTDIR:-$HOME}/.zshrc"', 'if [ -f "$ZSHRC" ]; then . "$ZSHRC"; fi'].join("; "); + return ['ZSHRC="${ZDOTDIR:-$HOME}/.zshrc"', 'if [ -f "$ZSHRC" ]; then { . "$ZSHRC"; } >/dev/null 2>&1; fi'].join( + "; " + ); case "bash": - return ['BASHRC="${BASH_ENV:-$HOME/.bashrc}"', 'if [ -f "$BASHRC" ]; then . "$BASHRC"; fi'].join("; "); + return [ + 'BASHRC="${BASH_ENV:-$HOME/.bashrc}"', + 'if [ -f "$BASHRC" ]; then { . "$BASHRC"; } >/dev/null 2>&1; fi', + ].join("; "); default: return null; } diff --git a/src/session.ts b/src/session.ts index 5e00b49e..ad89b5fe 100644 --- a/src/session.ts +++ b/src/session.ts @@ -66,6 +66,7 @@ const PROJECT_CODE_HASH_LENGTH = 16; const BACKGROUND_FAILURE_LOG_TAIL_CHARS = 4000; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; +const PLAN_MODE_STATUS_MESSAGE = "Set Plan Mode on. Awaiting ."; type ChatCompletionDebugOptions = { enabled?: boolean; @@ -1022,6 +1023,25 @@ ${agentInstructions} }); } + private appendSkillMessages(sessionId: string, skills?: SkillInfo[]): void { + if (!skills || skills.length === 0) { + return; + } + + for (const skill of skills) { + if (skill.name === "plan") { + this.appendSessionMessage(sessionId, this.buildSystemMessage(sessionId, PLAN_MODE_STATUS_MESSAGE)); + } + if (skill.isLoaded) { + continue; + } + const skillPrompt = this.buildSkillPrompt(skill); + const skillMessage = this.buildSkillMessage(sessionId, skillPrompt, skill); + this.appendSessionMessage(sessionId, skillMessage); + this.onAssistantMessage(skillMessage, true); + } + } + getActiveSessionId(): string | null { return this.activeSessionId; } @@ -1145,17 +1165,7 @@ ${agentInstructions} userPrompt.skills = await this.normalizeSkills(userPrompt.skills); this.throwIfAborted(signal); - if (userPrompt.skills && userPrompt.skills.length > 0) { - for (const skill of userPrompt.skills) { - if (skill.isLoaded) { - continue; - } - const skillPrompt = this.buildSkillPrompt(skill); - const skillMessage = this.buildSkillMessage(sessionId, skillPrompt, skill); - this.appendSessionMessage(sessionId, skillMessage); - this.onAssistantMessage(skillMessage, true); - } - } + this.appendSkillMessages(sessionId, userPrompt.skills); this.activeSessionId = sessionId; await this.activateSession(sessionId, controller); @@ -1220,17 +1230,7 @@ ${agentInstructions} userPrompt.skills = await this.normalizeSkills(userPrompt.skills, sessionId); this.throwIfAborted(signal); - if (userPrompt.skills && userPrompt.skills.length > 0) { - for (const skill of userPrompt.skills) { - if (skill.isLoaded) { - continue; - } - const skillPrompt = this.buildSkillPrompt(skill); - const skillMessage = this.buildSkillMessage(sessionId, skillPrompt, skill); - this.appendSessionMessage(sessionId, skillMessage); - this.onAssistantMessage(skillMessage, true); - } - } + this.appendSkillMessages(sessionId, userPrompt.skills); this.activeSessionId = sessionId; await this.activateSession(sessionId, controller); } @@ -2372,17 +2372,7 @@ ${agentInstructions} } userPrompt.skills = await this.normalizeSkills(userPrompt.skills, sessionId); this.throwIfAborted(signal); - if (userPrompt.skills && userPrompt.skills.length > 0) { - for (const skill of userPrompt.skills) { - if (skill.isLoaded) { - continue; - } - const skillPrompt = this.buildSkillPrompt(skill); - const skillMessage = this.buildSkillMessage(sessionId, skillPrompt, skill); - this.appendSessionMessage(sessionId, skillMessage); - this.onAssistantMessage(skillMessage, true); - } - } + this.appendSkillMessages(sessionId, userPrompt.skills); } private buildToolParamsSnippet(toolFunction: unknown | null): string { diff --git a/src/tests/message-view.test.ts b/src/tests/message-view.test.ts index 7d6b781c..ff497707 100644 --- a/src/tests/message-view.test.ts +++ b/src/tests/message-view.test.ts @@ -105,6 +105,10 @@ function makeSessionMessage(overrides: Partial & Pick { const msg = makeSessionMessage({ role: "user", content: "hello", visible: false }); assert.equal(renderMessageToStdout(msg, RawMode.Raw), ""); @@ -128,7 +132,7 @@ test("MessageView echoes submitted user prompts with live prompt wrapping width" const msg = makeSessionMessage({ role: "user", content: "abcdefg" }); const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 }); - assert.equal(output, "> abcdef\n g\n"); + assert.equal(stripAnsi(output), "> abcdef\n g\n"); }); test("MessageView echoes model changes with submitted prompt wrapping", () => { @@ -139,7 +143,7 @@ test("MessageView echoes model changes with submitted prompt wrapping", () => { }); const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 }); - assert.equal(output, "> abcdef\n gh\n"); + assert.equal(stripAnsi(output), "> abcdef\n gh\n"); }); test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => { diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index abebed3c..40877579 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -6,13 +6,14 @@ import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/file-history"; import { clearSessionState } from "../common/state"; -import { getProjectCode, SessionManager, type SessionMessage } from "../session"; +import { getProjectCode, SessionManager, type SessionMessage, type SkillInfo } from "../session"; const originalFetch = globalThis.fetch; const originalConsoleWarn = console.warn; const originalHome = process.env.HOME; const originalUserProfile = process.env.USERPROFILE; const tempDirs: string[] = []; +const PLAN_MODE_STATUS_MESSAGE = "Set Plan Mode on. Awaiting ."; /** Set homedir in a cross-platform way (HOME on Unix, USERPROFILE on Windows). */ function setHomeDir(dir: string): void { @@ -522,6 +523,70 @@ test("SessionManager resolves bundled skill prompts", () => { assert.match(prompt, /# Skill Writer/); }); +test("SessionManager appends plan mode status whenever the plan skill is selected", async () => { + const workspace = createTempDir("deepcode-plan-skill-workspace-"); + const home = createTempDir("deepcode-plan-skill-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-plan-skill"); + const planSkill = await getPlanSkill(manager); + + const sessionId = await manager.createSession({ text: "", skills: [planSkill] }); + let messages = manager.listSessionMessages(sessionId); + assert.equal(countPlanModeStatusMessages(messages), 1); + assert.equal(countLoadedSkillMessages(messages, "plan"), 1); + + await manager.replySession(sessionId, { text: "", skills: [planSkill] }); + messages = manager.listSessionMessages(sessionId); + assert.equal(countPlanModeStatusMessages(messages), 2); + assert.equal(countLoadedSkillMessages(messages, "plan"), 1); +}); + +test("SessionManager appends plan mode status when the plan skill is auto-matched", async () => { + const workspace = createTempDir("deepcode-plan-matched-workspace-"); + const home = createTempDir("deepcode-plan-matched-home-"); + setHomeDir(home); + + const client = { + chat: { + completions: { + create: async (request: any) => { + if (isSkillMatchingRequest(request)) { + return createSkillMatchingResponse(["plan"]); + } + return createChatResponse("planned", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }); + }, + }, + }, + }; + const manager = createMockedClientSessionManagerWithClient(workspace, client); + + const sessionId = await manager.createSession({ text: "Plan Mode for this change" }); + const messages = manager.listSessionMessages(sessionId); + assert.equal(countPlanModeStatusMessages(messages), 1); + assert.equal(countLoadedSkillMessages(messages, "plan"), 1); +}); + +test("SessionManager appends plan mode status for deferred permission prompts", async () => { + const workspace = createTempDir("deepcode-plan-deferred-workspace-"); + const home = createTempDir("deepcode-plan-deferred-home-"); + setHomeDir(home); + + const manager = createSessionManager(workspace, "machine-id-plan-deferred"); + const sessionId = await manager.createSession({ text: "" }); + const planSkill = await getPlanSkill(manager); + + await (manager as any).appendDeferredPermissionPrompt( + sessionId, + { text: "", skills: [planSkill] }, + new AbortController() + ); + + const messages = manager.listSessionMessages(sessionId); + assert.equal(countPlanModeStatusMessages(messages), 1); + assert.equal(countLoadedSkillMessages(messages, "plan"), 1); +}); + test("SessionManager excludes disabled skills by resolved skill name", async () => { const workspace = createTempDir("deepcode-disabled-skills-workspace-"); const home = createTempDir("deepcode-disabled-skills-home-"); @@ -564,6 +629,7 @@ test("SessionManager excludes disabled skills by resolved skill name", async () "renamed-disabled": false, "deepcode-self-refer": false, "skill-digester": false, + plan: false, "enabled-skill": true, }, }), @@ -3400,6 +3466,20 @@ function createSessionManager(projectRoot: string, machineId: string): SessionMa }); } +async function getPlanSkill(manager: SessionManager): Promise { + const planSkill = (await manager.listSkills()).find((skill) => skill.name === "plan"); + assert.ok(planSkill); + return planSkill; +} + +function countPlanModeStatusMessages(messages: SessionMessage[]): number { + return messages.filter((message) => message.role === "system" && message.content === PLAN_MODE_STATUS_MESSAGE).length; +} + +function countLoadedSkillMessages(messages: SessionMessage[], skillName: string): number { + return messages.filter((message) => message.role === "system" && message.meta?.skill?.name === skillName).length; +} + function createNotifyingSessionManager( projectRoot: string, responses: unknown[], @@ -3536,8 +3616,8 @@ function isSkillMatchingRequest(request: any): boolean { return request?.response_format?.type === "json_object"; } -function createSkillMatchingResponse(): unknown { - return { choices: [{ message: { content: '{"skillNames":[]}' } }] }; +function createSkillMatchingResponse(skillNames: string[] = []): unknown { + return { choices: [{ message: { content: JSON.stringify({ skillNames }) } }] }; } function createChatResponse(content: string, usage: Record): unknown { diff --git a/src/tests/shell-utils.test.ts b/src/tests/shell-utils.test.ts index 50a71f41..8ec56bb1 100644 --- a/src/tests/shell-utils.test.ts +++ b/src/tests/shell-utils.test.ts @@ -2,6 +2,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildDisableExtglobCommand, + buildShellInitCommand, getShellKind, posixPathToWindowsPath, resolveWindowsGitBashPath, @@ -39,6 +40,18 @@ test("Shell kind detection supports Windows bash.exe paths", () => { assert.equal(buildDisableExtglobCommand("/bin/zsh"), "setopt NO_EXTENDED_GLOB 2>/dev/null || true"); }); +test("Shell init commands suppress startup file output", () => { + assert.equal( + buildShellInitCommand("/bin/zsh"), + 'ZSHRC="${ZDOTDIR:-$HOME}/.zshrc"; if [ -f "$ZSHRC" ]; then { . "$ZSHRC"; } >/dev/null 2>&1; fi' + ); + assert.equal( + buildShellInitCommand("/bin/bash"), + 'BASHRC="${BASH_ENV:-$HOME/.bashrc}"; if [ -f "$BASHRC" ]; then { . "$BASHRC"; } >/dev/null 2>&1; fi' + ); + assert.equal(buildShellInitCommand("/bin/fish"), null); +}); + test("Windows Git Bash detection prefers bash.exe from PATH", () => { const bashPath = "D:\\Tools\\Git\\bin\\bash.exe"; const resolved = resolveWindowsGitBashPath({ diff --git a/templates/skills/bundled/plan/SKILL.md b/templates/skills/bundled/plan/SKILL.md new file mode 100644 index 00000000..b73c1abc --- /dev/null +++ b/templates/skills/bundled/plan/SKILL.md @@ -0,0 +1,133 @@ +--- +name: plan +description: Plan tasks through a strict non-mutating collaboration workflow before implementation. Use ONLY when the user asks for Plan Mode, planning only or non-mutating exploration. +--- + +# Plan Mode (Conversational) + +You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed—intent- and implementation-wise—so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions. + +## Mode rules (strict) + +You are in **Plan Mode** until a developer message explicitly ends it. + +Plan Mode is not changed by user intent, tone, or imperative language. If a user asks for execution while still in Plan Mode, treat it as a request to **plan the execution**, not perform it. + +## Plan Mode vs UpdatePlan tool + +Plan Mode is a collaboration mode that can involve requesting user input and eventually issuing a `` block. + +Separately, `UpdatePlan` is Deep Code's checklist/progress tool. It updates the current task plan with a complete markdown task list, but it does not enter or exit Plan Mode and it is not the final planning artifact. Do not use `UpdatePlan` as a substitute for the `` block. + +## Execution vs. mutation in Plan Mode + +You may explore and execute **non-mutating** actions that improve the plan. You must not perform **mutating** actions. + +### Allowed (non-mutating, plan-improving) + +Actions that gather truth, reduce ambiguity, or validate feasibility without changing repo-tracked state. Examples: + +* Reading or searching files, configs, schemas, types, manifests, and docs +* Static analysis, inspection, and repo exploration +* Dry-run style commands when they do not edit repo-tracked files +* Tests, builds, or checks that may write to caches or build artifacts (for example, `target/`, `.cache/`, or snapshots) so long as they do not edit repo-tracked files + +### Not allowed (mutating, plan-executing) + +Actions that implement the plan or change repo-tracked state. Examples: + +* Editing or writing files +* Running formatters or linters that rewrite files +* Applying patches, migrations, or codegen that updates repo-tracked files +* Side-effectful commands whose purpose is to carry out the plan rather than refine it + +When in doubt: if the action would reasonably be described as "doing the work" rather than "planning the work," do not do it. + +## PHASE 1 — Ground in the environment (explore first, ask second) + +Begin by grounding yourself in the actual environment. Eliminate unknowns in the prompt by discovering facts, not by asking the user. Resolve all questions that can be answered through exploration or inspection. Identify missing or ambiguous details only if they cannot be derived from the environment. Silent exploration between turns is allowed and encouraged. + +Before asking the user any question, perform at least one targeted non-mutating exploration pass (for example: search relevant files, inspect likely entrypoints/configs, confirm current implementation shape), unless no local environment/repo is available. + +Exception: you may ask clarifying questions about the user's prompt before exploring, ONLY if there are obvious ambiguities or contradictions in the prompt itself. However, if ambiguity might be resolved by exploring, always prefer exploring first. + +Do not ask questions that can be answered from the repo or system (for example, "where is this struct?" or "which UI component should we use?" when exploration can make it clear). Only ask once you have exhausted reasonable non-mutating exploration. + +## PHASE 2 — Intent chat (what they actually want) + +* Keep asking until you can clearly state: goal + success criteria, audience, in/out of scope, constraints, current state, and the key preferences/tradeoffs. +* Bias toward questions over guessing: if any high-impact ambiguity remains, do NOT plan yet—ask. + +## PHASE 3 — Implementation chat (what/how we’ll build) + +* Once intent is stable, keep asking until the spec is decision complete: approach, interfaces (APIs/schemas/I/O), data flow, edge cases/failure modes, testing + acceptance criteria, rollout/monitoring, and any migrations/compat constraints. + +## Asking questions + +Critical rules: + +* Strongly prefer using the `AskUserQuestion` tool to ask any questions. +* Offer only meaningful multiple‑choice options; don’t include filler choices that are obviously wrong or irrelevant. +* In rare cases where an unavoidable, important question can’t be expressed with reasonable multiple‑choice options (due to extreme ambiguity), you may ask it directly without the tool. + +You SHOULD ask many questions, but each question must: + +* materially change the spec/plan, OR +* confirm/lock an assumption, OR +* choose between meaningful tradeoffs. +* not be answerable by non-mutating commands. + +Use the `AskUserQuestion` tool only for decisions that materially change the plan, for confirming important assumptions, or for information that cannot be discovered via non-mutating exploration. Ask one question at a time when possible, provide concrete options with `label` and optional `description`, and use `multiSelect` only when multiple choices can be combined. + +## Two kinds of unknowns (treat differently) + +1. **Discoverable facts** (repo/system truth): explore first. + + * Before asking, run targeted searches and check likely sources of truth (configs/manifests/entrypoints/schemas/types/constants). + * Ask only if: multiple plausible candidates; nothing found but you need a missing identifier/context; or ambiguity is actually product intent. + * If asking, present concrete candidates (paths/service names) + recommend one. + * Never ask questions you can answer from your environment (e.g., “where is this struct”). + +2. **Preferences/tradeoffs** (not discoverable): ask early. + + * These are intent or implementation preferences that cannot be derived from exploration. + * Provide 2–4 mutually exclusive options + a recommended default. + * If unanswered, proceed with the recommended option and record it as an assumption in the final plan. + +## Finalization rule + +Only output the final plan when it is decision complete and leaves no decisions to the implementer. + +When you present the official plan, wrap it in a `` block so the client can render it specially: + +1) The opening tag must be on its own line. +2) Start the plan content on the next line (no text on the same line as the tag). +3) The closing tag must be on its own line. +4) Use Markdown inside the block. +5) Keep the tags exactly as `` and `` (do not translate or rename them), even if the plan content is in another language. + +Example: + + +plan content + + +plan content should be human and agent digestible. The final plan must be plan-only, concise by default, and include: + +* A clear title +* A brief summary section +* Important changes or additions to public APIs/interfaces/types +* Test cases and scenarios +* Explicit assumptions and defaults chosen where needed + +When possible, prefer a compact structure with 3-5 short sections, usually: Summary, Key Changes or Implementation Changes, Test Plan, and Assumptions. Do not include a separate Scope section unless scope boundaries are genuinely important to avoid mistakes. + +Prefer grouped implementation bullets by subsystem or behavior over file-by-file inventories. Mention files only when needed to disambiguate a non-obvious change, and avoid naming more than 3 paths unless extra specificity is necessary to prevent mistakes. Prefer behavior-level descriptions over symbol-by-symbol removal lists. For v1 feature-addition plans, do not invent detailed schema, validation, precedence, fallback, or wire-shape policy unless the request establishes it or it is needed to prevent a concrete implementation mistake; prefer the intended capability and minimum interface/behavior changes. + +Keep bullets short and avoid explanatory sub-bullets unless they are needed to prevent ambiguity. Prefer the minimum detail needed for implementation safety, not exhaustive coverage. Within each section, compress related changes into a few high-signal bullets and omit branch-by-branch logic, repeated invariants, and long lists of unaffected behavior unless they are necessary to prevent a likely implementation mistake. Avoid repeated repo facts and irrelevant edge-case or rollout detail. For straightforward refactors, keep the plan to a compact summary, key edits, tests, and assumptions. If the user asks for more detail, then expand. + +Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a `` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan. + +Only produce at most one `` block per turn, and only when you are presenting a complete spec. + +If the user stays in Plan mode and asks for revisions after a prior ``, any new `` must be a complete replacement. diff --git a/templates/skills/bundled/skill-writer/SKILL.md b/templates/skills/bundled/skill-writer/SKILL.md index e30bfdfc..1a7801c3 100644 --- a/templates/skills/bundled/skill-writer/SKILL.md +++ b/templates/skills/bundled/skill-writer/SKILL.md @@ -1,6 +1,6 @@ --- name: skill-writer -description: Guide users through creating, updating, debugging, and validating Agent Skills for AI agents. Use when the user wants to create, write, author, design, troubleshoot, validate, or improve a Skill, or needs help with SKILL.md files, frontmatter, or skill structure. +description: Guide users through creating, updating, debugging, and validating Agent Skills for AI agents. Use when the user wants to create, write, author, design, troubleshoot, validate, or improve a Skill, or needs help with SKILL.md, frontmatter, or skill structure. --- # Skill Writer From 7dba5131ef7296059550fc8f38fef2c9b6569cd3 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 11 Jun 2026 21:34:14 +0800 Subject: [PATCH 151/212] feat: update PLAN_MODE_STATUS_MESSAGE --- src/session.ts | 2 +- src/tests/session.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/session.ts b/src/session.ts index ad89b5fe..383f3490 100644 --- a/src/session.ts +++ b/src/session.ts @@ -66,7 +66,7 @@ const PROJECT_CODE_HASH_LENGTH = 16; const BACKGROUND_FAILURE_LOG_TAIL_CHARS = 4000; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; -const PLAN_MODE_STATUS_MESSAGE = "Set Plan Mode on. Awaiting ."; +const PLAN_MODE_STATUS_MESSAGE = "/plan\n └ Set Plan Mode on. Awaiting ."; type ChatCompletionDebugOptions = { enabled?: boolean; diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 40877579..8df048b4 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -13,7 +13,7 @@ const originalConsoleWarn = console.warn; const originalHome = process.env.HOME; const originalUserProfile = process.env.USERPROFILE; const tempDirs: string[] = []; -const PLAN_MODE_STATUS_MESSAGE = "Set Plan Mode on. Awaiting ."; +const PLAN_MODE_STATUS_MESSAGE = "/plan\n └ Set Plan Mode on. Awaiting ."; /** Set homedir in a cross-platform way (HOME on Unix, USERPROFILE on Windows). */ function setHomeDir(dir: string): void { From 07cfe21464520d1dd71d23dffac3d317e10a9fd6 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 12 Jun 2026 10:00:57 +0800 Subject: [PATCH 152/212] feat: add support for implicit invocation control in skills --- src/prompt.ts | 19 ++++++++++- src/session.ts | 18 ++++++++-- src/tests/prompt.test.ts | 21 ++++++++++++ src/tests/session.test.ts | 72 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/prompt.ts b/src/prompt.ts index c5c888c6..3b18ba6f 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import ejs from "ejs"; +import matter from "gray-matter"; import { fileURLToPath } from "url"; import type { SessionMessage } from "./session"; import { findGitBashPath, resolveShellPath } from "./common/shell-utils"; @@ -184,11 +185,27 @@ export function buildSkillDocumentsPrompt(skills: SkillPromptDocument[]): string function renderSkillDocumentBlock(skill: SkillPromptDocument): string { const pathAttribute = skill.path ? ` path="${escapeXml(skill.path)}"` : ""; const resources = renderSkillResources(skill.skillFilePath); + const content = stripSkillPromptMetadata(skill.content); return `<${skill.name}-skill${pathAttribute}> -${skill.content}${resources} +${content}${resources} `; } +function stripSkillPromptMetadata(content: string): string { + try { + const parsed = matter(content); + if (!Object.prototype.hasOwnProperty.call(parsed.data, "metadata")) { + return content; + } + + const frontmatter = { ...parsed.data }; + delete frontmatter.metadata; + return matter.stringify(parsed.content, frontmatter); + } catch { + return content; + } +} + function renderSkillResources(skillFilePath?: string): string { if (!skillFilePath) { return ""; diff --git a/src/session.ts b/src/session.ts index 383f3490..6a594b2f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -289,6 +289,7 @@ export type SkillInfo = { path: string; description: string; isLoaded?: boolean; + allowImplicitInvocation?: boolean; }; type SessionManagerOptions = { @@ -732,13 +733,14 @@ Response in JSON format: If none of the available skills match, respond with an empty array, i.e. \`{"skillNames": []}\`.\n `; const simpleSkills = skills - .filter((x) => !x.isLoaded) + .filter((x) => !x.isLoaded && x.allowImplicitInvocation !== false) .map((x) => { return { name: x.name, description: x.description }; }); if (simpleSkills.length === 0) { return []; } + const candidateSkillNames = new Set(simpleSkills.map((skill) => skill.name)); const { client, model, baseURL, debugLogEnabled } = this.createOpenAIClient(); if (!client) { @@ -787,7 +789,10 @@ ${agentInstructions} const parsed = JSON.parse(content); if (parsed && Array.isArray(parsed.skillNames)) { - return parsed.skillNames; + return parsed.skillNames.filter( + (skillName: unknown): skillName is string => + typeof skillName === "string" && candidateSkillNames.has(skillName) + ); } return []; @@ -938,6 +943,14 @@ ${agentInstructions} try { const skillMd = fs.readFileSync(skillPath, "utf8"); const parsed = matter(skillMd); + const metadata = parsed.data.metadata; + const allowImplicitInvocation = + metadata && + typeof metadata === "object" && + !Array.isArray(metadata) && + (metadata as Record)["allow-implicit-invocation"] === false + ? false + : undefined; return { name: typeof parsed.data.name === "string" && parsed.data.name.trim() @@ -945,6 +958,7 @@ ${agentInstructions} : fallbackSkill.name, path: displayPath, description: typeof parsed.data.description === "string" ? parsed.data.description.trim() : "", + allowImplicitInvocation, }; } catch { return fallbackSkill; diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts index cef6c620..899a2b26 100644 --- a/src/tests/prompt.test.ts +++ b/src/tests/prompt.test.ts @@ -88,6 +88,27 @@ test("getDefaultSkillPrompt loads the default skill template", () => { assert.equal(prompt.includes('path="templates/skills/'), false); }); +test("buildSkillDocumentsPrompt excludes SKILL.md frontmatter metadata", () => { + const prompt = buildSkillDocumentsPrompt([ + { + name: "example", + content: + "---\nname: example\ndescription: Example skill\nlicense: MIT\ncompatibility: Node.js\nallowed-tools: Read Bash\nmetadata:\n author: test\n allow-implicit-invocation: false\n---\n# Example Skill\n\nUse these instructions.\n", + }, + ]); + + assert.equal(prompt.includes("name: example"), true); + assert.equal(prompt.includes("description: Example skill"), true); + assert.equal(prompt.includes("license: MIT"), true); + assert.equal(prompt.includes("compatibility: Node.js"), true); + assert.equal(prompt.includes("allowed-tools: Read Bash"), true); + assert.equal(prompt.includes("# Example Skill"), true); + assert.equal(prompt.includes("Use these instructions."), true); + assert.equal(prompt.includes("metadata:"), false); + assert.equal(prompt.includes("author: test"), false); + assert.equal(prompt.includes("allow-implicit-invocation"), false); +}); + test("buildSkillDocumentsPrompt lists skill resources", () => { const skillDir = createTempDir("deepcode-skill-resources-"); fs.mkdirSync(path.join(skillDir, "scripts"), { recursive: true }); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 8df048b4..38767e43 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -644,6 +644,78 @@ test("SessionManager excludes disabled skills by resolved skill name", async () assert.equal(skills[0]?.path, "./.deepcode/skills/enabled-skill/SKILL.md"); }); +test("SessionManager keeps implicit opt-out skills available for manual invocation", async () => { + const workspace = createTempDir("deepcode-manual-only-skill-workspace-"); + const home = createTempDir("deepcode-manual-only-skill-home-"); + setHomeDir(home); + + const skillDir = path.join(workspace, ".agents", "skills", "manual-only"); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync( + path.join(skillDir, "SKILL.md"), + "---\nname: manual-only\ndescription: Manual-only skill\nmetadata:\n allow-implicit-invocation: false\n---\n# Manual Only\n", + "utf8" + ); + + const manager = createSessionManager(workspace, "machine-id-manual-only-skill"); + const skill = (await manager.listSkills()).find((candidate) => candidate.name === "manual-only"); + assert.ok(skill); + assert.equal(skill.allowImplicitInvocation, false); + + const sessionId = await manager.createSession({ text: "", skills: [skill] }); + const skillMessages = manager + .listSessionMessages(sessionId) + .filter((message) => message.role === "system" && message.meta?.skill?.name === "manual-only"); + + assert.equal(skillMessages.length, 1); + assert.match(skillMessages[0]?.content ?? "", / { + const workspace = createTempDir("deepcode-implicit-opt-out-workspace-"); + const home = createTempDir("deepcode-implicit-opt-out-home-"); + setHomeDir(home); + globalThis.fetch = (async () => ({ ok: true, text: async () => "" }) as Response) as typeof fetch; + + const writeSkill = (name: string, metadata = ""): void => { + const skillDir = path.join(workspace, ".deepcode", "skills", name); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync( + path.join(skillDir, "SKILL.md"), + `---\nname: ${name}\ndescription: ${name} description${metadata}\n---\n# ${name}\n`, + "utf8" + ); + }; + writeSkill("auto-skill"); + writeSkill("manual-only", "\nmetadata:\n allow-implicit-invocation: false"); + + const requests: any[] = []; + const client = { + chat: { + completions: { + create: async (request: any) => { + requests.push(request); + if (isSkillMatchingRequest(request)) { + return createSkillMatchingResponse(["manual-only", "auto-skill"]); + } + return createChatResponse("done", { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }); + }, + }, + }, + }; + const manager = createMockedClientSessionManagerWithClient(workspace, client); + (manager as any).activateSession = async () => {}; + + const sessionId = await manager.createSession({ text: "choose an automatic skill" }); + const matchingPrompt = String(requests[0]?.messages?.[0]?.content ?? ""); + + assert.match(matchingPrompt, /"name": "auto-skill"/); + assert.doesNotMatch(matchingPrompt, /"name": "manual-only"/); + assert.equal(countLoadedSkillMessages(manager.listSessionMessages(sessionId), "auto-skill"), 1); + assert.equal(countLoadedSkillMessages(manager.listSessionMessages(sessionId), "manual-only"), 0); +}); + test("SessionManager dispose disconnects MCP servers", async () => { const workspace = createTempDir("deepcode-mcp-dispose-workspace-"); const serverPath = path.join(workspace, "mcp-server.cjs"); From 849bab4589ee16eb4f781a45beff832d65ec7adc Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 12 Jun 2026 14:13:38 +0800 Subject: [PATCH 153/212] feat: add agent-skills.md --- docs/agent-skills.md | 322 ++++++++++++++++++++++++++++++++++++++++ docs/agent-skills_en.md | 322 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 644 insertions(+) create mode 100644 docs/agent-skills.md create mode 100644 docs/agent-skills_en.md diff --git a/docs/agent-skills.md b/docs/agent-skills.md new file mode 100644 index 00000000..b1bd4e45 --- /dev/null +++ b/docs/agent-skills.md @@ -0,0 +1,322 @@ +# Deep Code CLI Agent Skills 指南 + +## 概述 + +适合写成 skill 的内容通常具备以下特点: + +- 会重复使用,例如固定的代码审查流程、发布流程或文档生成流程 +- 需要较长的说明,不适合每次都粘贴到对话中 +- 需要配套资源,例如模板、脚本、schema、示例或参考文档 +- 需要明确触发条件,例如“处理 PDF 表单”或“为本项目生成数据库迁移” + +不适合写成 skill 的内容: + +- 一次性的任务要求 +- 当前仓库的短规则,此类内容更适合写入 `AGENTS.md` +- 需要实时连接外部系统的能力,此类能力更适合通过 MCP 提供工具 + +## 扫描位置 + +Deep Code CLI 会按以下顺序扫描 skills。相同 `name` 的 skill 只保留优先级最高的一个。 + +| 优先级 | Scope | Path | 用途 | +| ------ | ------- | --------------------- | ---- | +| 1 | Project | `./.deepcode/skills/` | Deep Code 项目级原生位置 | +| 2 | Project | `./.agents/skills/` | 项目级跨客户端互操作位置 | +| 3 | User | `~/.deepcode/skills/` | Deep Code 用户级原生位置 | +| 4 | User | `~/.agents/skills/` | 用户级跨客户端互操作位置 | +| 5 | Global | `built-in` | Deep Code 内置 skills | + +目录结构示例: + +```text +.deepcode/ +└── skills/ + └── code-review/ + ├── SKILL.md + ├── checklist.md + └── scripts/ + └── collect-diff.sh +``` + +## 最小 Skill + +每个 skill 必须放在独立目录中,并包含 `SKILL.md`。 + +```markdown +--- +name: code-review +description: Review code changes for correctness, regressions, security risks, and missing tests. Use when the user asks for a review, PR review, diff review, or pre-merge check. +--- + +# Code Review + +Use a code review mindset. Prioritize bugs, behavioral regressions, security issues, +and missing tests over style comments. + +## Workflow + +1. Inspect the diff and relevant surrounding code. +2. List findings first, ordered by severity. +3. Include file and line references for every finding. +4. If there are no findings, say so and mention residual risks or test gaps. +``` + +## `SKILL.md` Frontmatter + +Deep Code CLI reads the YAML frontmatter at the top of `SKILL.md`. + +| 字段 | 必填 | Deep Code 行为 | 建议 | +| ---- | ---- | -------------- | ---- | +| `name` | 建议必填 | 作为 skill 的唯一名称。缺失时使用目录名,并把 `_` 转成 `-`。 | 使用小写字母、数字和连字符。保持与目录名一致。 | +| `description` | 建议必填 | 用于自动匹配任务,也显示在 `/skills` 和斜杠菜单中。 | 写清楚 skill 做什么、何时使用、常见触发词。 | +| `metadata.allow-implicit-invocation` | 可选 | 设置为 `false` 时,不参与自动匹配;仍可手动选择。 | 用于只想手动调用的 skill。 | + +示例: + +```yaml +--- +name: db-migration +description: Create and review database migrations for this project. Use when the user asks to add columns, change schema, write migrations, or validate rollback behavior. +metadata: + allow-implicit-invocation: false +--- +``` + +> Deep Code CLI 当前只解释上表中的字段。其他 frontmatter 字段可用于跨客户端互操作或文档说明,但不会自动限制 Deep Code 的工具权限。 + +## 写好 `description` + +`description` 是最重要的发现信号。Deep Code 会在自动匹配阶段只把 skill 的 `name` 和 `description` 交给模型判断,因此描述越具体,匹配越可靠。 + +推荐结构: + +```text +<这个 skill 做什么>. Use when <任务类型、文件类型、领域、用户常见说法或触发词>. +``` + +好的示例: + +```yaml +description: Extract tables from PDF files, fill PDF forms, and merge documents. Use when working with PDFs, forms, invoices, statements, or document extraction. +``` + +```yaml +description: Generate Lessweb routes, services, and Pydantic request models. Use when editing Lessweb projects, adding @Get/@Post endpoints, configuring IOC modules, or updating OpenAPI output. +``` + +避免: + +```yaml +description: Helps with documents +description: Useful project skill +description: Tooling instructions +``` + +检查清单: + +- 是否说明了具体能力,而不是只写主题名 +- 是否说明了何时使用,而不是只写结果 +- 是否包含用户可能输入的关键词 +- 是否包含相关文件类型、框架名、命令名或领域名 +- 是否避免覆盖过宽,导致无关任务也触发 + +## Skill 正文结构 + +`SKILL.md` 的正文应面向 agent,而不是面向普通读者。写法要直接、可执行、可验证。 + +推荐结构: + +```markdown +# Skill Name + +Briefly state what this skill is for. + +## When to use + +- Use when ... +- Do not use when ... + +## Workflow + +1. Read ... +2. Run ... +3. Edit ... +4. Verify ... + +## Rules + +- Preserve ... +- Never ... +- Ask the user when ... + +## Examples + +... +``` + +写作原则: + +- 使用命令式步骤,例如“Read the schema first”或“Run tests after editing” +- 把必须遵守的约束写成明确规则 +- 对高风险操作写清楚边界,例如删除文件、迁移数据、发送请求 +- 对常见分支写出决策规则,例如“如果没有配置文件,先搜索默认路径” +- 避免把大量参考资料全部塞进 `SKILL.md` + +## 附加资源 + +一个 skill 可以包含 `SKILL.md` 之外的文件: + +```text +my-skill/ +├── SKILL.md +├── references/ +│ └── api.md +├── examples/ +│ └── request.json +├── scripts/ +│ └── validate.py +└── templates/ + └── report.md +``` + +如果某些内容会让 `SKILL.md` 过长或难以阅读,可以放到附加文件中: + +- `references/` 放长文档、规范、API 说明 +- `examples/` 放输入输出样例 +- `scripts/` 放可复用脚本 +- `templates/` 放文档或代码模板 +- 在 `SKILL.md` 中说明什么时候需要使用这些附加文件 + +示例: + +```markdown +## Workflow + +1. Read `references/schema.md` before changing generated types. +2. Use `templates/migration.sql` when creating a new migration. +3. Run `python scripts/check_migration.py ` before reporting completion. +``` + +## 调用方式 + +Deep Code CLI 支持自动和手动两种调用方式。 + +### 自动调用 + +每次用户输入后,Deep Code 会根据可用 skills 的 `name` 和 `description` 判断哪些 skill 与任务匹配。匹配到的 skill 会被加载到当前会话中。 + +自动调用规则: + +- 已加载的 skill 不会在同一会话中重复加载 +- `metadata.allow-implicit-invocation: false` 的 skill 不会自动加载 +- 自动匹配会结合当前 `AGENTS.md` 指令 +- 如果没有匹配项,则不加载 skill + +### 手动调用 + +你可以在输入框中使用 `/` 打开 skills / 命令菜单,选择某个 skill;也可以使用 `/skills` 查看可用 skills。 + +常用命令: + +| 命令 | 作用 | +| ---- | ---- | +| `/` | 打开 skills / 命令菜单 | +| `/skills` | 列出可用 skills | +| `/` | 从菜单中选择对应 skill | + +## 启用和禁用 + +使用 `settings.json` 的 `enabledSkills` 可以按 skill 名称启用或禁用 skill。 + +```json +{ + "enabledSkills": { + "code-review": true, + "db-migration": false + } +} +``` + +规则: + +- 未配置的 skill 默认启用 +- 设置为 `false` 会隐藏所有扫描位置中同名的 skill +- 项目设置会按 skill 覆盖用户设置 + +更多配置说明请参考 [configuration.md](configuration.md)。 + +## 与 `AGENTS.md`、MCP 的区别 + +| 机制 | 适合放什么 | 不适合放什么 | +| ---- | ---------- | ------------ | +| `AGENTS.md` | 当前仓库的长期规则、代码风格、测试命令、协作约定 | 可复用的复杂工作流或跨项目工具说明 | +| Agent Skill | 可复用工作流、领域知识、模板、脚本、参考资料 | 只对当前一次任务生效的临时要求 | +| MCP | 外部系统能力、实时数据、浏览器、数据库、GitHub 等工具调用 | 纯文本流程说明 | + +常见组合: + +- 把项目规则写进 `AGENTS.md` +- 把可复用流程写成 skill +- 把需要执行外部动作的能力接入 MCP + +## 编写示例:项目发布 Skill + +```markdown +--- +name: release-check +description: Prepare and verify a project release. Use when the user asks to release, publish, bump version, update changelog, or run pre-release checks. +--- + +# Release Check + +Use this skill to prepare a safe release for this repository. + +## Workflow + +1. Read `package.json` and the existing changelog. +2. Inspect commits or diffs since the previous release tag. +3. Update version and changelog only when the user explicitly asks. +4. Run the project test and build commands. +5. Report the version, changed files, verification results, and remaining risks. + +## Rules + +- Do not publish packages unless the user explicitly asks. +- Do not create or push git tags without explicit approval. +- Preserve existing changelog style. +``` + +## 故障排查 + +### `/skills` 中看不到 skill + +检查: + +1. 目录是否位于 Deep Code 扫描位置之一 +2. 文件名是否为 `SKILL.md` +3. `SKILL.md` 是否在独立 skill 目录中,例如 `.deepcode/skills/my-skill/SKILL.md` +4. `enabledSkills` 是否把该 skill 设置为 `false` +5. 是否存在同名 skill 被更高优先级位置覆盖 + +### 自动调用不稳定 + +检查: + +1. `description` 是否包含清晰的使用场景和触发词 +2. skill 是否过宽,导致模型难以判断边界 +3. 是否设置了 `metadata.allow-implicit-invocation: false` +4. 用户请求是否需要更明确地提到该 skill 的领域或文件类型 + +### Skill 内容过长 + +建议: + +1. 保留 `SKILL.md` 中的核心流程和规则 +2. 把长文档移到 `references/` +3. 把重复命令移到 `scripts/` +4. 在 `SKILL.md` 中说明何时读取相关文件 + +## 参考 + +- [Agent Skills Specification](https://agentskills.io/specification) diff --git a/docs/agent-skills_en.md b/docs/agent-skills_en.md new file mode 100644 index 00000000..50f3a2a5 --- /dev/null +++ b/docs/agent-skills_en.md @@ -0,0 +1,322 @@ +# Deep Code CLI Agent Skills Guide + +## Overview + +A good skill is useful when the instruction set is: + +- Reused across tasks, such as code review, release preparation, or report generation +- Too long or detailed to paste into every prompt +- Backed by resources, such as templates, scripts, schemas, examples, or reference docs +- Triggered by a clear situation, such as "process a PDF form" or "create a database migration for this project" + +Do not use a skill for: + +- One-off task requirements +- Short repository rules, which usually belong in `AGENTS.md` +- Live external actions, which usually belong in MCP tools + +## Scan Locations + +Deep Code CLI scans skills in the following order. If multiple skills resolve to the same `name`, only the highest-priority one is kept. + +| Priority | Scope | Path | Purpose | +| -------- | ------- | --------------------- | ------- | +| 1 | Project | `./.deepcode/skills/` | Native Deep Code project skills | +| 2 | Project | `./.agents/skills/` | Project skills shared with other agent clients | +| 3 | User | `~/.deepcode/skills/` | Native Deep Code user skills | +| 4 | User | `~/.agents/skills/` | User skills shared with other agent clients | +| 5 | Global | `built-in` | Skills bundled with Deep Code | + +Example structure: + +```text +.deepcode/ +└── skills/ + └── code-review/ + ├── SKILL.md + ├── checklist.md + └── scripts/ + └── collect-diff.sh +``` + +## Minimal Skill + +Each skill must live in its own directory and contain `SKILL.md`. + +```markdown +--- +name: code-review +description: Review code changes for correctness, regressions, security risks, and missing tests. Use when the user asks for a review, PR review, diff review, or pre-merge check. +--- + +# Code Review + +Use a code review mindset. Prioritize bugs, behavioral regressions, security issues, +and missing tests over style comments. + +## Workflow + +1. Inspect the diff and relevant surrounding code. +2. List findings first, ordered by severity. +3. Include file and line references for every finding. +4. If there are no findings, say so and mention residual risks or test gaps. +``` + +## `SKILL.md` Frontmatter + +Deep Code CLI reads YAML frontmatter at the top of `SKILL.md`. + +| Field | Required | Deep Code behavior | Recommendation | +| ----- | -------- | ------------------ | -------------- | +| `name` | Recommended | Used as the unique skill name. If missing, Deep Code uses the directory name and converts `_` to `-`. | Use lowercase letters, numbers, and hyphens. Keep it aligned with the directory name. | +| `description` | Recommended | Used for automatic matching and shown in `/skills` and the slash menu. | Describe what the skill does, when to use it, and common trigger terms. | +| `metadata.allow-implicit-invocation` | Optional | When set to `false`, the skill is excluded from automatic matching but can still be selected manually. | Use for manual-only skills. | + +Example: + +```yaml +--- +name: db-migration +description: Create and review database migrations for this project. Use when the user asks to add columns, change schema, write migrations, or validate rollback behavior. +metadata: + allow-implicit-invocation: false +--- +``` + +> Deep Code CLI currently interprets only the fields listed above. Other frontmatter fields may be useful for cross-client compatibility or documentation, but they do not automatically restrict Deep Code tool permissions. + +## Write a Strong `description` + +The `description` is the most important discovery signal. During automatic matching, Deep Code gives the model only each skill's `name` and `description`, so specific descriptions match more reliably. + +Recommended pattern: + +```text +. Use when . +``` + +Good examples: + +```yaml +description: Extract tables from PDF files, fill PDF forms, and merge documents. Use when working with PDFs, forms, invoices, statements, or document extraction. +``` + +```yaml +description: Generate Lessweb routes, services, and Pydantic request models. Use when editing Lessweb projects, adding @Get/@Post endpoints, configuring IOC modules, or updating OpenAPI output. +``` + +Avoid: + +```yaml +description: Helps with documents +description: Useful project skill +description: Tooling instructions +``` + +Checklist: + +- State the concrete capability, not only the topic +- State when to use it, not only the expected result +- Include terms users are likely to type +- Include relevant file types, framework names, command names, or domain names +- Avoid overbroad wording that triggers on unrelated tasks + +## Skill Body Structure + +The body of `SKILL.md` should be written for an agent, not for a general reader. Keep it direct, actionable, and verifiable. + +Recommended structure: + +```markdown +# Skill Name + +Briefly state what this skill is for. + +## When to use + +- Use when ... +- Do not use when ... + +## Workflow + +1. Read ... +2. Run ... +3. Edit ... +4. Verify ... + +## Rules + +- Preserve ... +- Never ... +- Ask the user when ... + +## Examples + +... +``` + +Writing principles: + +- Use imperative steps, such as "Read the schema first" or "Run tests after editing" +- Write mandatory constraints as explicit rules +- Define boundaries for high-risk operations, such as deleting files, migrating data, or sending requests +- Document common branches, such as "if no config file exists, search the default paths first" +- Move long reference material out of `SKILL.md` + +## Supporting Resources + +A skill can include files next to `SKILL.md`: + +```text +my-skill/ +├── SKILL.md +├── references/ +│ └── api.md +├── examples/ +│ └── request.json +├── scripts/ +│ └── validate.py +└── templates/ + └── report.md +``` + +Use supporting files for material that would make `SKILL.md` too long or too hard to scan: + +- Put long docs, specs, and API notes in `references/` +- Put input and output samples in `examples/` +- Put reusable commands in `scripts/` +- Put document or code skeletons in `templates/` +- Explain in `SKILL.md` when each supporting file is relevant + +Example: + +```markdown +## Workflow + +1. Read `references/schema.md` before changing generated types. +2. Use `templates/migration.sql` when creating a new migration. +3. Run `python scripts/check_migration.py ` before reporting completion. +``` + +## Invocation + +Deep Code CLI supports automatic and manual skill invocation. + +### Automatic Invocation + +After each user message, Deep Code checks the available skills' `name` and `description` fields and selects the skills that match the task. Matching skills are loaded into the current session. + +Automatic invocation rules: + +- A loaded skill is not loaded again in the same session +- A skill with `metadata.allow-implicit-invocation: false` is not loaded automatically +- Matching considers the current `AGENTS.md` instructions +- If no skill matches, no skill is loaded + +### Manual Invocation + +Type `/` in the input box to open the skills and commands menu, then select a skill. Use `/skills` to list available skills. + +Common commands: + +| Command | Behavior | +| ------- | -------- | +| `/` | Open the skills and commands menu | +| `/skills` | List available skills | +| `/` | Select the matching skill from the menu | + +## Enable and Disable Skills + +Use `enabledSkills` in `settings.json` to enable or disable skills by name. + +```json +{ + "enabledSkills": { + "code-review": true, + "db-migration": false + } +} +``` + +Rules: + +- Skills not listed are enabled by default +- Setting a skill to `false` hides every scanned skill with that resolved name +- Project settings override user settings per skill + +For more details, see [configuration_en.md](configuration_en.md). + +## Skills vs. `AGENTS.md` vs. MCP + +| Mechanism | Best for | Not best for | +| --------- | -------- | ------------ | +| `AGENTS.md` | Long-lived repository rules, coding style, test commands, collaboration conventions | Reusable complex workflows or cross-project tool instructions | +| Agent Skill | Reusable workflows, domain knowledge, templates, scripts, reference docs | Temporary requirements for a single task | +| MCP | External systems, live data, browser control, databases, GitHub, and other tool calls | Pure text workflow instructions | + +Common pattern: + +- Put repository rules in `AGENTS.md` +- Put reusable workflows in skills +- Put external actions behind MCP tools + +## Example: Project Release Skill + +```markdown +--- +name: release-check +description: Prepare and verify a project release. Use when the user asks to release, publish, bump version, update changelog, or run pre-release checks. +--- + +# Release Check + +Use this skill to prepare a safe release for this repository. + +## Workflow + +1. Read `package.json` and the existing changelog. +2. Inspect commits or diffs since the previous release tag. +3. Update version and changelog only when the user explicitly asks. +4. Run the project test and build commands. +5. Report the version, changed files, verification results, and remaining risks. + +## Rules + +- Do not publish packages unless the user explicitly asks. +- Do not create or push git tags without explicit approval. +- Preserve existing changelog style. +``` + +## Troubleshooting + +### The skill does not appear in `/skills` + +Check: + +1. The directory is under one of the Deep Code scan locations +2. The file is named `SKILL.md` +3. `SKILL.md` is inside its own skill directory, such as `.deepcode/skills/my-skill/SKILL.md` +4. `enabledSkills` has not set the skill to `false` +5. A higher-priority skill with the same name is not shadowing it + +### Automatic invocation is unreliable + +Check: + +1. The `description` contains clear use cases and trigger terms +2. The skill is not so broad that the model cannot infer its boundary +3. `metadata.allow-implicit-invocation` is not set to `false` +4. The user request mentions the relevant domain or file type clearly enough + +### The skill is too long + +Recommendations: + +1. Keep the core workflow and rules in `SKILL.md` +2. Move long documentation into `references/` +3. Move repeatable commands into `scripts/` +4. Explain when the agent should read each supporting file + +## References + +- [Agent Skills Specification](https://agentskills.io/specification) From fb4aaa88fcb5c2f0df5b3a6db190119686c3702b Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 12 Jun 2026 16:07:42 +0800 Subject: [PATCH 154/212] feat: add agents-md.md and quickstart.md --- docs/agent-skills.md | 2 +- docs/agent-skills_en.md | 2 +- docs/agents-md.md | 220 ++++++++++++++++++++++++++++++++++++++++ docs/agents-md_en.md | 220 ++++++++++++++++++++++++++++++++++++++++ docs/quickstart.md | 197 +++++++++++++++++++++++++++++++++++ docs/quickstart_en.md | 197 +++++++++++++++++++++++++++++++++++ 6 files changed, 836 insertions(+), 2 deletions(-) create mode 100644 docs/agents-md.md create mode 100644 docs/agents-md_en.md create mode 100644 docs/quickstart.md create mode 100644 docs/quickstart_en.md diff --git a/docs/agent-skills.md b/docs/agent-skills.md index b1bd4e45..9ba77ab5 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -1,4 +1,4 @@ -# Deep Code CLI Agent Skills 指南 +# Agent Skills ## 概述 diff --git a/docs/agent-skills_en.md b/docs/agent-skills_en.md index 50f3a2a5..13d2ad7b 100644 --- a/docs/agent-skills_en.md +++ b/docs/agent-skills_en.md @@ -1,4 +1,4 @@ -# Deep Code CLI Agent Skills Guide +# Agent Skills ## Overview diff --git a/docs/agents-md.md b/docs/agents-md.md new file mode 100644 index 00000000..9888e7c9 --- /dev/null +++ b/docs/agents-md.md @@ -0,0 +1,220 @@ +# AGENTS.md + +`AGENTS.md` 是写给 AI 编码助手的项目说明文件。它用于记录长期有效的项目规则,让 Deep Code 在这个仓库中工作时知道如何安装依赖、运行测试、修改代码、提交变更,以及遵守哪些团队约定。 + +如果你经常在提示词里重复说明“先运行哪个测试”“不要修改哪个目录”“PR 描述要包含什么”,就适合把这些内容写进 `AGENTS.md`。 + +## 适合写什么 + +`AGENTS.md` 适合保存当前项目的稳定规则: + +- 项目结构和重要目录 +- 安装、开发、构建、测试命令 +- 代码风格、命名约定、格式化要求 +- 测试要求和验证步骤 +- 提交、PR、发布流程 +- 安全、配置、凭据处理注意事项 +- 只对本仓库有效的 AI 协作规则 + +不适合写入 `AGENTS.md` 的内容: + +- 一次性任务要求,例如“这次只改登录页” +- 复杂可复用工作流,此类内容更适合写成 Agent Skill +- 外部系统连接信息,此类能力更适合通过 MCP 配置 +- API Key、密码、Token 等敏感信息 + +## 创建文件 + +在项目中运行: + +```text +/init +``` + +Deep Code 会帮助你创建或更新 `AGENTS.md`。你也可以手动创建: + +```bash +touch AGENTS.md +``` + +如果你希望把说明放在 Deep Code 专用目录中,也可以使用: + +```bash +mkdir -p .deepcode +touch .deepcode/AGENTS.md +``` + +常见选择: + +| 文件 | 适合场景 | +| ---- | -------- | +| `AGENTS.md` | 希望项目中的不同 AI 编码工具都能读取 | +| `.deepcode/AGENTS.md` | 希望规则只面向 Deep Code | +| `~/.deepcode/AGENTS.md` | 个人默认偏好,适用于没有项目说明的仓库 | + +## 推荐结构 + +保持简短、清晰、可执行。推荐从下面这些章节开始: + +```markdown +# 仓库指南 + +## 项目结构 + +说明主要目录,以及新增代码应该放在哪里。 + +## 开发命令 + +- `npm install` — 安装依赖。 +- `npm test` — 运行测试套件。 +- `npm run build` — 构建项目。 + +## 代码风格 + +说明格式化、命名和框架约定。 + +## 测试 + +说明什么时候需要添加测试,以及需要运行哪些命令。 + +## Pull Request + +说明提交风格、PR 检查项、截图或发布说明要求。 + +## AI 助手注意事项 + +列出 AI 助手需要遵守的规则,例如不要修改哪些文件,或完成前要执行哪些检查。 +``` + +你不需要保留所有章节。只写对当前项目有帮助的内容。 + +## 写作原则 + +### 写具体命令 + +推荐: + +```markdown +## 开发命令 + +- `npm install` — 安装依赖。 +- `npm test` — 运行全部测试。 +- `npm run build` — 执行类型检查并构建 CLI。 +``` + +避免: + +```markdown +完成前运行常用命令。 +``` + +### 写明确规则 + +推荐: + +```markdown +## 测试 + +修改行为时需要新增或更新测试。完成前,测试相关修改运行 +`npm test`,代码修改运行 `npm run build`。 +``` + +避免: + +```markdown +确保一切正常。 +``` + +### 写项目事实 + +推荐: + +```markdown +## 项目结构 + +- `src/` 存放应用代码。 +- `tests/` 存放自动化测试。 +- `docs/` 存放面向用户的文档。 +``` + +避免: + +```markdown +这是一个普通的 TypeScript 项目。 +``` + +### 写安全边界 + +推荐: + +```markdown +## 安全 + +不要提交 API Key 或 Token。本地凭据放在 `~/.deepcode/settings.json`, +项目示例中的敏感信息需要脱敏。 +``` + +避免: + +```markdown +注意保护密钥。 +``` + +## 示例 + +下面是一个较完整的 `AGENTS.md` 示例: + +```markdown +# 仓库指南 + +## 项目结构 + +- `src/` 存放应用代码。 +- `src/tests/` 存放自动化测试。 +- `docs/` 存放面向用户的文档。 +- `config/` 存放项目配置示例。 + +## 开发命令 + +- `npm install` — 安装依赖。 +- `npm test` — 运行自动化测试。 +- `npm run build` — 运行检查并构建 CLI。 + +## 代码风格 + +使用 TypeScript。保持代码清晰,优先使用明确命名,并遵循现有 +格式风格。不要引入无关重构。 + +## 测试 + +修改行为时需要添加测试。优先运行范围最小的相关测试;条件允许时, +完成前运行 `npm test` 或 `npm run build`。 + +## AI 助手注意事项 + +- 不要提交密钥或本地生成文件。 +- 保留用户已有修改。 +- 说明无法执行的验证步骤。 +``` + +## 与 Skills、MCP 的区别 + +| 工具 | 适合放什么 | +| ---- | ---------- | +| `AGENTS.md` | 当前仓库的长期规则、命令、风格、验证步骤 | +| Agent Skill | 可复用工作流、领域知识、模板、脚本、参考资料 | +| MCP | GitHub、浏览器、数据库等外部工具和实时数据 | + +常见组合: + +- 在 `AGENTS.md` 中写“这个项目怎么做” +- 在 Agent Skill 中写“这类任务怎么做” +- 用 MCP 提供“需要连接外部服务才能做”的能力 + +## 维护建议 + +- 项目命令变化后及时更新 +- 删除已经过期的规则 +- 避免写太长,优先保留高频、重要、容易出错的约定 +- 不要写敏感信息 +- 如果规则只适用于某次任务,直接写在当前对话中即可 diff --git a/docs/agents-md_en.md b/docs/agents-md_en.md new file mode 100644 index 00000000..e63047b1 --- /dev/null +++ b/docs/agents-md_en.md @@ -0,0 +1,220 @@ +# AGENTS.md + +`AGENTS.md` is a project instruction file for AI coding assistants. Use it to record long-lived repository rules so Deep Code knows how to install dependencies, run tests, edit code, prepare changes, and follow team conventions. + +If you often repeat instructions such as "run this test first", "do not edit that directory", or "include these details in the PR summary", put them in `AGENTS.md`. + +## What to Include + +Use `AGENTS.md` for stable project rules: + +- Project structure and important directories +- Install, development, build, and test commands +- Coding style, naming conventions, and formatting rules +- Testing expectations and verification steps +- Commit, pull request, and release conventions +- Security, configuration, and credential handling notes +- AI collaboration rules that apply only to this repository + +Do not use `AGENTS.md` for: + +- One-off task requirements, such as "only edit the login page this time" +- Complex reusable workflows, which are better as Agent Skills +- External service connections, which are better configured with MCP +- API keys, passwords, tokens, or other secrets + +## Create the File + +Run this inside a project: + +```text +/init +``` + +Deep Code helps create or update `AGENTS.md`. You can also create it manually: + +```bash +touch AGENTS.md +``` + +If you want Deep Code-specific project instructions, you can use: + +```bash +mkdir -p .deepcode +touch .deepcode/AGENTS.md +``` + +Common choices: + +| File | Best for | +| ---- | -------- | +| `AGENTS.md` | Rules that should be visible to multiple AI coding tools | +| `.deepcode/AGENTS.md` | Rules intended only for Deep Code | +| `~/.deepcode/AGENTS.md` | Personal defaults for repositories without project instructions | + +## Recommended Structure + +Keep it short, clear, and actionable. Start with sections like these: + +```markdown +# Repository Guidelines + +## Project Structure + +Describe the main directories and where new code should go. + +## Development Commands + +- `npm install` — Install dependencies. +- `npm test` — Run the test suite. +- `npm run build` — Build the project. + +## Coding Style + +Describe formatting, naming, and framework conventions. + +## Testing + +Explain when to add tests and which commands to run. + +## Pull Requests + +Describe commit style, PR checklist, screenshots, or release notes. + +## Agent Notes + +List rules for AI assistants, such as files to avoid or checks to run before finishing. +``` + +You do not need every section. Keep only what helps in this repository. + +## Writing Principles + +### Write Concrete Commands + +Good: + +```markdown +## Development Commands + +- `npm install` — Install dependencies. +- `npm test` — Run all tests. +- `npm run build` — Type-check and build the CLI. +``` + +Avoid: + +```markdown +Run the usual commands before finishing. +``` + +### Write Explicit Rules + +Good: + +```markdown +## Testing + +Add or update tests when changing behavior. Before reporting completion, run +`npm test` for test-only changes and `npm run build` for code changes. +``` + +Avoid: + +```markdown +Make sure everything works. +``` + +### Write Repository Facts + +Good: + +```markdown +## Project Structure + +- `src/` contains application code. +- `tests/` contains automated tests. +- `docs/` contains user-facing documentation. +``` + +Avoid: + +```markdown +This is a normal TypeScript project. +``` + +### Write Safety Boundaries + +Good: + +```markdown +## Security + +Do not commit API keys or tokens. Use `~/.deepcode/settings.json` for local +credentials and keep project examples redacted. +``` + +Avoid: + +```markdown +Be careful with secrets. +``` + +## Example + +Here is a complete `AGENTS.md` example: + +```markdown +# Repository Guidelines + +## Project Structure + +- `src/` contains application code. +- `src/tests/` contains automated tests. +- `docs/` contains user-facing documentation. +- `config/` contains project configuration examples. + +## Development Commands + +- `npm install` — Install dependencies. +- `npm test` — Run automated tests. +- `npm run build` — Run checks and build the CLI. + +## Coding Style + +Use TypeScript. Keep code readable, prefer clear names, and follow the existing +formatting style. Do not introduce unrelated refactors. + +## Testing + +Add tests when changing behavior. Run the narrowest relevant test first, then +run `npm test` or `npm run build` before reporting completion when practical. + +## Agent Notes + +- Do not commit secrets or generated local files. +- Preserve existing user changes. +- Explain any verification step that could not be run. +``` + +## AGENTS.md vs. Skills vs. MCP + +| Mechanism | Best for | +| --------- | -------- | +| `AGENTS.md` | Long-lived repository rules, commands, style, and verification steps | +| Agent Skill | Reusable workflows, domain knowledge, templates, scripts, and reference docs | +| MCP | External tools and live data, such as GitHub, browsers, or databases | + +Common pattern: + +- Put "how this project works" in `AGENTS.md` +- Put "how this type of task works" in an Agent Skill +- Use MCP for work that requires external services + +## Maintenance Tips + +- Update commands when the project changes +- Remove outdated rules +- Keep it concise; prioritize frequent, important, and easy-to-miss conventions +- Do not include secrets +- If a rule applies only to the current task, write it in the current conversation instead diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..8de1f3ce --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,197 @@ +# 快速开始 + +Deep Code 是一款开源的终端 AI 编程助手,专为 DeepSeek-V4 系列模型适配,支持深度思考、推理强度控制,并通过 Skills 和 MCP 扩展更多能力。 + +## 前置要求 + +使用前请确认本机已安装: + +- Node.js `22` 或更高版本 +- 一个可用的 DeepSeek API Key + +## 安装 + +使用 npm 全局安装: + +```bash +npm install -g @vegamo/deepcode-cli +``` + +安装后检查版本: + +```bash +deepcode --version +``` + +## 配置 DeepSeek-V4 + +Deep Code 推荐使用 `deepseek-v4-pro`,也支持 `deepseek-v4-flash`。创建 `~/.deepcode/settings.json`,写入你的 DeepSeek 模型配置: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max" +} +``` + +把 `API_KEY` 替换成你的 DeepSeek API Key。 + +常用字段: + +| 字段 | 说明 | +| ---- | ---- | +| `env.MODEL` | DeepSeek 模型名称,推荐 `deepseek-v4-pro` | +| `env.BASE_URL` | DeepSeek API 地址,默认 `https://api.deepseek.com` | +| `env.API_KEY` | DeepSeek API Key | +| `thinkingEnabled` | 是否启用思考模式 | +| `reasoningEffort` | 推理强度,常用 `"high"` 或 `"max"` | + +也可以在项目目录中创建 `.deepcode/settings.json`,为当前项目单独设置模型、权限或 MCP。 + +更多 DeepSeek 官方配置说明可参考 [Deep Code 集成指南](https://api-docs.deepseek.com/zh-cn/quick_start/agent_integrations/deepcode)。 + +更多配置项请参考 [configuration.md](configuration.md)。 + +## 启动 + +进入你的项目目录: + +```bash +cd path/to/your/project +deepcode +``` + +Deep Code 会在当前目录中启动交互式界面。你可以直接输入任务,然后按 `Enter` 发送。 + +如果想带着初始问题启动: + +```bash +deepcode -p "总结这个项目" +``` + +## 第一次可以这样问 + +可以先从只读任务开始: + +```text +总结这个仓库,并说明如何运行它。 +``` + +```text +找出主要入口文件,并解释请求流程。 +``` + +然后尝试让 Deep Code 修改代码: + +```text +为登录校验逻辑添加一个单元测试。 +``` + +```text +运行测试用例,并修复失败的测试。 +``` + +也可以让它先给出计划: + +```text +在修改文件前,先给出一个为用户列表添加分页的计划。 +``` + +## 常用操作 + +| 操作 | 用法 | +| ---- | ---- | +| 发送消息 | `Enter` | +| 输入多行 | `Shift+Enter` 或 `Ctrl+J` | +| 中断当前回复 | `Esc` | +| 粘贴图片 | `Ctrl+V` | +| 退出 | 连续按两次 `Ctrl+D`,或使用 `/exit` | + +## 斜杠命令 + +在输入框中输入 `/` 可以打开命令菜单。 + +| 命令 | 作用 | +| ---- | ---- | +| `/new` | 开始新对话 | +| `/resume` | 选择历史对话继续 | +| `/continue` | 继续当前对话,或恢复最近的对话 | +| `/model` | 切换模型、思考模式和推理强度 | +| `/init` | 为当前项目生成 `AGENTS.md` 指令文件 | +| `/skills` | 查看可用 Agent Skills | +| `/mcp` | 查看 MCP 服务状态和可用工具 | +| `/undo` | 将代码和/或对话恢复到之前的状态 | +| `/raw` | 切换显示模式 | +| `/exit` | 退出 Deep Code | + +## 为项目添加说明 + +在项目中运行: + +```text +/init +``` + +Deep Code 会帮助你创建 `AGENTS.md`。这个文件适合记录项目约定,例如: + +- 项目如何安装依赖和运行测试 +- 代码风格和提交要求 +- 重要目录说明 +- 修改代码前后需要执行的检查 + +之后 Deep Code 在该项目中工作时会自动参考这些说明。 + +## 使用 Skills + +Agent Skills 适合保存可复用工作流,例如代码审查、发布检查、文档生成或某个框架的固定开发流程。 + +查看可用 skills: + +```text +/skills +``` + +也可以输入 `/`,在菜单中选择某个 skill。 + +更多说明请参考 [agent-skills.md](agent-skills.md)。 + +## 连接外部工具 + +如果你想让 Deep Code 连接 GitHub、浏览器、数据库或其他服务,可以配置 MCP。 + +配置后,在 Deep Code 中运行: + +```text +/mcp +``` + +即可查看已连接的 MCP 服务和可用工具。 + +更多说明请参考 [mcp.md](mcp.md)。 + +## 权限与安全 + +Deep Code 可能会读取文件、修改代码或运行命令。你可以通过权限配置控制哪些操作自动允许、哪些操作需要确认、哪些操作直接拒绝。 + +Deep Code 默认支持 YOLO 模式,可以更流畅地执行读写文件、运行命令等操作。如果你希望更谨慎,可以使用严格模式,让 Deep Code 在执行较高风险操作前询问你。 + +更多说明请参考 [permission.md](permission.md)。 + +## 任务完成通知 + +如果希望 Deep Code 完成任务后通知你,可以配置通知脚本,例如发送 Slack、飞书、系统通知或终端提示。 + +更多说明请参考 [notify.md](notify.md)。 + +## 下一步 + +- 阅读完整配置说明:[configuration.md](configuration.md) +- 配置权限策略:[permission.md](permission.md) +- 编写 Agent Skills:[agent-skills.md](agent-skills.md) +- 配置 MCP 外部工具:[mcp.md](mcp.md) +- 配置任务完成通知:[notify.md](notify.md) diff --git a/docs/quickstart_en.md b/docs/quickstart_en.md new file mode 100644 index 00000000..44f62692 --- /dev/null +++ b/docs/quickstart_en.md @@ -0,0 +1,197 @@ +# Quickstart + +Deep Code is an open-source terminal AI coding assistant for the DeepSeek-V4 model, supporting deep thinking, reasoning effort control, and extend its capabilities with Skills and MCP. + +## Prerequisites + +Before you start, make sure you have: + +- Node.js `22` or later +- A DeepSeek API key + +## Install + +Install Deep Code globally with npm: + +```bash +npm install -g @vegamo/deepcode-cli +``` + +Check the installed version: + +```bash +deepcode --version +``` + +## Configure DeepSeek-V4 + +Deep Code recommends `deepseek-v4-pro` and also supports `deepseek-v4-flash`. Create `~/.deepcode/settings.json` and add your DeepSeek model configuration: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max" +} +``` + +Replace `API_KEY` with your DeepSeek API key. + +Common fields: + +| Field | Description | +| ----- | ----------- | +| `env.MODEL` | DeepSeek model name, recommended `deepseek-v4-pro` | +| `env.BASE_URL` | DeepSeek API endpoint, default `https://api.deepseek.com` | +| `env.API_KEY` | DeepSeek API key | +| `thinkingEnabled` | Whether to enable thinking mode | +| `reasoningEffort` | Reasoning effort, commonly `"high"` or `"max"` | + +You can also create `.deepcode/settings.json` inside a project to customize the model, permissions, or MCP settings for that project only. + +For DeepSeek's official setup notes, see the [Deep Code integration guide](https://api-docs.deepseek.com/zh-cn/quick_start/agent_integrations/deepcode). + +For all configuration options, see [configuration_en.md](configuration_en.md). + +## Start + +Open your project directory: + +```bash +cd path/to/your/project +deepcode +``` + +Deep Code starts an interactive terminal UI in the current directory. Type a task and press `Enter`. + +To start with an initial prompt: + +```bash +deepcode -p "Summarize this project" +``` + +## Try These First + +Start with a read-only task: + +```text +Summarize this repository and explain how to run it. +``` + +```text +Find the main entry points and explain the request flow. +``` + +Then try a coding task: + +```text +Add a unit test for the login validation logic. +``` + +```text +Run the test suite and fix the failing tests. +``` + +You can also ask for a plan first: + +```text +Before editing files, propose a plan for adding pagination to the user list. +``` + +## Basic Controls + +| Action | Key | +| ------ | --- | +| Send message | `Enter` | +| Insert a newline | `Shift+Enter` or `Ctrl+J` | +| Interrupt the current response | `Esc` | +| Paste an image | `Ctrl+V` | +| Quit | Press `Ctrl+D` twice, or use `/exit` | + +## Slash Commands + +Type `/` in the input box to open the command menu. + +| Command | Action | +| ------- | ------ | +| `/new` | Start a new conversation | +| `/resume` | Choose a previous conversation to continue | +| `/continue` | Continue the current conversation or resume the latest one | +| `/model` | Switch model, thinking mode, and reasoning effort | +| `/init` | Create an `AGENTS.md` instruction file for the current project | +| `/skills` | Show available Agent Skills | +| `/mcp` | Show MCP server status and available tools | +| `/undo` | Restore code and/or conversation to an earlier point | +| `/raw` | Change the display mode | +| `/exit` | Quit Deep Code | + +## Add Project Instructions + +Run this inside a project: + +```text +/init +``` + +Deep Code helps create `AGENTS.md`. Use it to record project conventions, such as: + +- How to install dependencies and run tests +- Code style and contribution expectations +- Important directory notes +- Checks to run before or after editing code + +Deep Code automatically uses these instructions when working in the project. + +## Use Skills + +Agent Skills are reusable workflows, such as code review, release checks, documentation generation, or framework-specific development steps. + +List available skills: + +```text +/skills +``` + +You can also type `/` and choose a skill from the menu. + +For more details, see [agent-skills_en.md](agent-skills_en.md). + +## Connect External Tools + +Use MCP to connect Deep Code to GitHub, browsers, databases, or other services. + +After configuring MCP, run: + +```text +/mcp +``` + +This shows connected MCP servers and available tools. + +For setup instructions, see [mcp_en.md](mcp_en.md). + +## Permissions and Safety + +Deep Code may read files, edit code, or run commands. You can configure which actions are allowed automatically, which require confirmation, and which are denied. + +Deep Code supports YOLO mode by default, so it can smoothly read and write files, run commands, and continue common coding tasks. If you prefer a more cautious setup, use strict permissions so Deep Code asks before higher-risk actions. + +For details, see [permission_en.md](permission_en.md). + +## Task Completion Notifications + +Deep Code can run a notification script when a task finishes, such as sending a Slack message, Feishu message, system notification, or terminal alert. + +For examples, see [notify_en.md](notify_en.md). + +## Next Steps + +- Read the full configuration guide: [configuration_en.md](configuration_en.md) +- Configure permissions: [permission_en.md](permission_en.md) +- Write Agent Skills: [agent-skills_en.md](agent-skills_en.md) +- Configure MCP tools: [mcp_en.md](mcp_en.md) +- Configure task completion notifications: [notify_en.md](notify_en.md) From cb35887e7ee1fcc55dd5267cdfab2d1ac4d617a2 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Sat, 13 Jun 2026 00:02:30 +0800 Subject: [PATCH 155/212] feat: update karpathy-guidelines.md to enhance clarity on success criteria and implementation checks --- templates/skills/karpathy-guidelines.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/templates/skills/karpathy-guidelines.md b/templates/skills/karpathy-guidelines.md index ae47e9da..36137d81 100644 --- a/templates/skills/karpathy-guidelines.md +++ b/templates/skills/karpathy-guidelines.md @@ -10,8 +10,6 @@ Behavioral guidelines to reduce common LLM coding mistakes. **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. -**Internal use:** Apply these guidelines silently. Do not cite this document, its title, or guideline names in user-facing responses. - ## 1. Think Before Coding **Don't assume. Don't hide confusion. Surface tradeoffs.** @@ -54,6 +52,15 @@ The test: Every changed line should trace directly to the user's request. **Define success criteria. Loop until verified.** +Before implementing, define the exact observable acceptance check: +- Command output +- Test assertion +- UI state +- File diff +- API response + +Do not start implementation if "works" cannot be checked objectively. If the check is unclear and would change the solution, ask before coding using AskUserQuestion tool. + Transform tasks into verifiable goals: - "Add validation" → "Write tests for invalid inputs, then make them pass" - "Fix the bug" → "Write a test that reproduces it, then make it pass" @@ -67,3 +74,16 @@ For multi-step tasks, state a brief plan: ``` Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +## 5. No Proxy Success + +**Passing means the acceptance check passes.** + +Don't substitute weaker signals: +- "No crash" unless that was the goal. +- "Non-empty output" unless any output is valid. +- "Looks plausible" unless the task is subjective. +- "Some tests pass" while the target check fails. +- "Implementation complete" without verification. + +Report exact pass, partial progress, or failure. From b9117ec1c141bfb0f14efb48e6fb1b234bbc4197 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Sat, 13 Jun 2026 00:19:30 +0800 Subject: [PATCH 156/212] feat: update templates/skills/bundled/deepcode-self-refer/SKILL.md --- .../bundled/deepcode-self-refer/SKILL.md | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/templates/skills/bundled/deepcode-self-refer/SKILL.md b/templates/skills/bundled/deepcode-self-refer/SKILL.md index 96f275f8..56b902ef 100644 --- a/templates/skills/bundled/deepcode-self-refer/SKILL.md +++ b/templates/skills/bundled/deepcode-self-refer/SKILL.md @@ -58,9 +58,23 @@ Use the `Read` tool to read the appropriate document(s) from the list above. All ### Step 4: Handle common request patterns **"列出/查看可用的 skills":** -- Explain the skill scanning paths from references/README.md (`./.deepcode/skills/`, `./.agents/skills/`, `~/.deepcode/skills/`, `~/.agents/skills/`, and bundled built-in skills) -- Explain that `/skills` slash command lists available skills -- Mention `enabledSkills` in `settings.json` for enabling/disabling specific skills +- Treat `/skills` as the canonical UI for listing currently available skills. +- If answering directly, do not infer the list only from loaded skill prompts or from project/user directories. Enumerate all discovery roots: + 1. `./.deepcode/skills//SKILL.md` + 2. `./.agents/skills//SKILL.md` + 3. `~/.deepcode/skills//SKILL.md` + 4. `~/.agents/skills//SKILL.md` + 5. bundled built-in skills as `bundled:/SKILL.md` +- For a source checkout, bundled skills live under `templates/skills/bundled//SKILL.md`. For a packaged install, bundled skills may live under `dist/bundled//SKILL.md`. +- Read each candidate `SKILL.md` frontmatter to get the resolved `name` and `description`; the folder name is only a fallback. +- De-duplicate by resolved `name`, keeping the highest-priority root from the order above. +- Apply `enabledSkills` from `settings.json`: if `enabledSkills[""] === false`, do not list that skill as available. +- Clearly separate discoverable skills from other concepts: + - Discoverable skills are selectable through `/skills` and come from the roots above. + - Bundled skills are discoverable skills shipped with Deep Code, such as `bundled:deepcode-self-refer/SKILL.md`. + - Default prompt templates or always-injected guidance are not necessarily discoverable skills unless they also exist as `*/SKILL.md` in one of the scan roots. + - Slash commands such as `/skills`, `/mcp`, and `/undo` are commands, not skills. +- Mention that `/skills` can be used to verify the result and `enabledSkills` can enable/disable specific skills by name. **"配置 MCP":** - Read `references/mcp.md` for the MCP format and examples @@ -90,9 +104,11 @@ Use the `Read` tool to read the appropriate document(s) from the list above. All ### Example 1: "列出可用的skills" -Read references/README.md, locate the Skills section. Answer: +Read references/README.md, locate the Skills section, then enumerate all scan roots including bundled skills. Answer: - Skills are discovered from: `./.deepcode/skills/`, `./.agents/skills/`, `~/.deepcode/skills/`, `~/.agents/skills/`, and bundled built-in skills such as `bundled:deepcode-self-refer/SKILL.md`. +- In a source checkout, check `templates/skills/bundled/*/SKILL.md`; in a packaged install, check `dist/bundled/*/SKILL.md`. +- Built-in bundled skills may include `deepcode-self-refer`, `plan`, `skill-digester`, and `skill-writer`; verify the actual list by scanning the bundled root because it can change between versions. - Use `/skills` slash command in the Deep Code CLI to list all available skills - Use `enabledSkills` in `settings.json` to enable/disable skills by name From 1d5f6f36c8d775ab1fcb1247c49198aa542a9c11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 00:03:47 +0000 Subject: [PATCH 157/212] chore(deps-dev): bump esbuild and tsx Bumps [esbuild](https://github.com/evanw/esbuild) and [tsx](https://github.com/privatenumber/tsx). These dependencies needed to be updated together. Updates `esbuild` from 0.28.0 to 0.28.1 - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1) Updates `tsx` from 4.21.0 to 4.22.4 - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.21.0...v4.22.4) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.28.1 dependency-type: direct:development - dependency-name: tsx dependency-version: 4.22.4 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 730 +++++++--------------------------------------- 1 file changed, 111 insertions(+), 619 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09db34ab..6de20e9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -299,9 +299,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -316,9 +316,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -333,9 +333,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -350,9 +350,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -367,9 +367,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -384,9 +384,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -401,9 +401,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -418,9 +418,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -435,9 +435,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -452,9 +452,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -469,9 +469,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -486,9 +486,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -503,9 +503,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -520,9 +520,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -537,9 +537,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -554,9 +554,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -571,9 +571,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -588,9 +588,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -605,9 +605,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -622,9 +622,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -639,9 +639,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -656,9 +656,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -673,9 +673,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -690,9 +690,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -707,9 +707,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -724,9 +724,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1773,9 +1773,9 @@ ] }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1786,32 +1786,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -2238,19 +2238,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz", @@ -3247,16 +3234,6 @@ "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -3529,14 +3506,13 @@ } }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -3548,490 +3524,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", From 66cc09826fa3a5f616c1f1841caebc1aab38bcc0 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 15 Jun 2026 10:21:31 +0800 Subject: [PATCH 158/212] feat: update karpathy-guidelines.md --- templates/skills/karpathy-guidelines.md | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/templates/skills/karpathy-guidelines.md b/templates/skills/karpathy-guidelines.md index 36137d81..41e23d73 100644 --- a/templates/skills/karpathy-guidelines.md +++ b/templates/skills/karpathy-guidelines.md @@ -1,7 +1,6 @@ --- name: karpathy-guidelines description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria. -license: MIT --- # Karpathy Guidelines @@ -52,15 +51,6 @@ The test: Every changed line should trace directly to the user's request. **Define success criteria. Loop until verified.** -Before implementing, define the exact observable acceptance check: -- Command output -- Test assertion -- UI state -- File diff -- API response - -Do not start implementation if "works" cannot be checked objectively. If the check is unclear and would change the solution, ask before coding using AskUserQuestion tool. - Transform tasks into verifiable goals: - "Add validation" → "Write tests for invalid inputs, then make them pass" - "Fix the bug" → "Write a test that reproduces it, then make it pass" @@ -73,17 +63,4 @@ For multi-step tasks, state a brief plan: 3. [Step] → verify: [check] ``` -Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. - -## 5. No Proxy Success - -**Passing means the acceptance check passes.** - -Don't substitute weaker signals: -- "No crash" unless that was the goal. -- "Non-empty output" unless any output is valid. -- "Looks plausible" unless the task is subjective. -- "Some tests pass" while the target check fails. -- "Implementation complete" without verification. - -Report exact pass, partial progress, or failure. +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. \ No newline at end of file From 72d710ec9621c5258ef72a60459c16b9d68513e4 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 15 Jun 2026 10:44:21 +0800 Subject: [PATCH 159/212] feat: update default skill loading so enabledSkills can skip the built-in skills --- src/prompt.ts | 19 +- src/session.ts | 2 +- src/tests/prompt.test.ts | 9 +- src/tests/session.test.ts | 34 ++++ templates/skills/agent-drift-guard.md | 152 ---------------- templates/skills/plan-and-execute.md | 246 -------------------------- 6 files changed, 57 insertions(+), 405 deletions(-) delete mode 100644 templates/skills/agent-drift-guard.md delete mode 100644 templates/skills/plan-and-execute.md diff --git a/src/prompt.ts b/src/prompt.ts index 3b18ba6f..dce34940 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -100,6 +100,10 @@ type PromptToolOptions = { webSearchEnabled?: boolean; }; +type DefaultSkillPromptOptions = { + enabledSkills?: Record; +}; + const DEFAULT_SKILL_TEMPLATES = ["karpathy-guidelines.md"]; const DEFAULT_SKILL_RESOURCE_FILE_LIMIT = 50; const SKILL_RESOURCE_EXCLUDED_DIRS = new Set([ @@ -153,13 +157,20 @@ function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): s return docs.join("\n\n"); } -function readDefaultSkillDocs(extensionRoot: string): Array<{ name: string; content: string }> { +function readDefaultSkillDocs( + extensionRoot: string, + enabledSkills: Record = {} +): Array<{ name: string; content: string }> { const skillsDir = path.join(extensionRoot, "templates", "skills"); return DEFAULT_SKILL_TEMPLATES.map((entry) => { const fullPath = path.join(skillsDir, entry); + const name = path.basename(entry, ".md"); + if (enabledSkills[name] === false) { + return null; + } try { return { - name: path.basename(entry, ".md"), + name, content: fs.readFileSync(fullPath, "utf8").trim(), }; } catch { @@ -168,8 +179,8 @@ function readDefaultSkillDocs(extensionRoot: string): Array<{ name: string; cont }).filter((skill): skill is { name: string; content: string } => Boolean(skill?.content)); } -export function getDefaultSkillPrompt(): string { - const skillDocs = readDefaultSkillDocs(getExtensionRoot()); +export function getDefaultSkillPrompt(options: DefaultSkillPromptOptions = {}): string { + const skillDocs = readDefaultSkillDocs(getExtensionRoot(), options.enabledSkills); if (skillDocs.length === 0) { return ""; } diff --git a/src/session.ts b/src/session.ts index 6a594b2f..3460cea5 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1142,7 +1142,7 @@ ${agentInstructions} const systemMessage = this.buildSystemMessage(sessionId, systemPrompt); this.appendSessionMessage(sessionId, systemMessage); - const defaultSkillPrompt = getDefaultSkillPrompt(); + const defaultSkillPrompt = getDefaultSkillPrompt({ enabledSkills: this.getResolvedSettings().enabledSkills }); if (defaultSkillPrompt) { const defaultSkillMessage = this.buildSystemMessage(sessionId, defaultSkillPrompt); this.appendSessionMessage(sessionId, defaultSkillMessage); diff --git a/src/tests/prompt.test.ts b/src/tests/prompt.test.ts index 899a2b26..6b474c1e 100644 --- a/src/tests/prompt.test.ts +++ b/src/tests/prompt.test.ts @@ -88,6 +88,12 @@ test("getDefaultSkillPrompt loads the default skill template", () => { assert.equal(prompt.includes('path="templates/skills/'), false); }); +test("getDefaultSkillPrompt skips disabled default skills", () => { + const prompt = getDefaultSkillPrompt({ enabledSkills: { "karpathy-guidelines": false } }); + + assert.equal(prompt, ""); +}); + test("buildSkillDocumentsPrompt excludes SKILL.md frontmatter metadata", () => { const prompt = buildSkillDocumentsPrompt([ { @@ -200,8 +206,7 @@ test("runtime prompt assets live under templates", () => { assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "web-search.md")), true); assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "read.md.ejs")), true); assert.equal(fs.existsSync(path.join(repoRoot, "templates", "prompts", "init_command.md.ejs")), true); - assert.equal(fs.existsSync(path.join(repoRoot, "templates", "skills", "agent-drift-guard.md")), true); - assert.equal(fs.existsSync(path.join(repoRoot, "templates", "skills", "plan-and-execute.md")), true); + assert.equal(fs.existsSync(path.join(repoRoot, "templates", "skills", "karpathy-guidelines.md")), true); assert.equal(fs.existsSync(path.join(repoRoot, "templates", "tools", "read.md")), false); assert.equal(fs.existsSync(path.join(repoRoot, "docs", "tools")), false); assert.equal(fs.existsSync(path.join(repoRoot, "docs", "prompts")), false); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 38767e43..57b981bb 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -1138,6 +1138,40 @@ test("createSession appends default system prompts in prefix-cache-friendly orde assert.equal(systemContents[3], "root project instructions"); }); +test("createSession skips disabled default skills", async () => { + const workspace = createTempDir("deepcode-disabled-default-skill-workspace-"); + const home = createTempDir("deepcode-disabled-default-skill-home-"); + setHomeDir(home); + + const manager = new SessionManager({ + projectRoot: workspace, + createOpenAIClient: () => ({ + client: null, + model: "test-model", + baseURL: "https://api.deepseek.com", + thinkingEnabled: false, + machineId: "machine-id-disabled-default-skill", + }), + getResolvedSettings: () => ({ + model: "test-model", + enabledSkills: { "karpathy-guidelines": false }, + }), + renderMarkdown: (text) => text, + onAssistantMessage: () => {}, + }); + + const sessionId = await manager.createSession({ text: "hello" }); + const systemContents = manager + .listSessionMessages(sessionId) + .filter((message) => message.role === "system") + .map((message) => message.content ?? ""); + + assert.equal(systemContents.length, 2); + assert.match(systemContents[0] ?? "", /# Available Tools/); + assert.doesNotMatch(systemContents.join("\n"), //); + assert.match(systemContents[1] ?? "", /# Local Workspace Environment/); +}); + test("createSession includes agent instructions in the skill matching system prompt", async () => { const workspace = createTempDir("deepcode-skill-match-create-workspace-"); const home = createTempDir("deepcode-skill-match-create-home-"); diff --git a/templates/skills/agent-drift-guard.md b/templates/skills/agent-drift-guard.md deleted file mode 100644 index c6711b12..00000000 --- a/templates/skills/agent-drift-guard.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -name: agent-drift-guard -description: Detect and correct execution drift while working on user requests. Use when you are actively implementing, debugging, reviewing, or investigating and there is a risk of wandering beyond the user's goal, adding unrequested work, touching live systems, over-exploring, or ignoring repeated user boundary corrections. Especially useful during multi-step coding tasks, production-adjacent requests, ambiguous scopes, and anytime you should self-check whether it is still solving the requested problem. ---- - -# Agent Drift Guard - -Keep execution tightly aligned with the user's actual request. - -## Quick Start - -Run this mental check before substantial work and again whenever the plan expands: - -1. State the user's requested outcome in one sentence. -2. List explicit non-goals or boundaries the user has set. -3. Ask whether the next action directly advances the requested outcome. -4. If not, either cut it or pause to confirm. - -## Drift Signals - -Treat these as warning signs that execution may be drifting: - -- Exploring broadly before opening the most relevant file, command, or artifact. -- Solving adjacent operational issues when the user asked only for code changes. -- Adding extra safeguards, scripts, docs, refactors, or cleanup that the user did not ask for. -- Reframing the task around what seems "better" instead of what was requested. -- Continuing with a broader plan after the user narrows the scope. -- Repeating searches or tool calls without increasing certainty. -- Mixing diagnosis, remediation, and feature work when the user asked for only one of them. -- Touching production-like state, external systems, or live data without explicit permission. - -## Severity Levels - -### Level 1: Mild Drift - -Examples: -- One or two extra exploratory commands. -- Considering a broader solution but not acting on it yet. -- Briefly over-explaining instead of moving the task forward. - -Response: -- Auto-correct silently. -- Narrow to the smallest next action. -- Do not interrupt the user. - -### Level 2: Material Drift - -Examples: -- Planning additional deliverables not requested. -- Writing helper scripts, migrations, docs, or tests outside the asked scope. -- Expanding from code changes into operational fixes. -- Continuing after the user has already corrected the scope once. - -Response: -- Stop and realign internally first. -- If the broader action is avoidable, drop it and continue on scope. -- If the broader action has non-obvious tradeoffs, ask a brief confirmation question. - -### Level 3: Boundary or Risk Violation - -Examples: -- Modifying live systems, production data, external services, or user-owned state without being asked. -- Taking destructive or hard-to-reverse actions outside the requested scope. -- Ignoring repeated user instructions about what not to do. - -Response: -- Pause before acting. -- Surface the exact boundary and ask for confirmation. -- Offer the smallest on-scope option first. - -## Self-Check Loop - -Use this loop during execution: - -### Before the first meaningful action - -Write down mentally: -- Requested outcome -- Allowed scope -- Forbidden scope -- Smallest useful next step - -### After each non-trivial step - -Ask: -- Did this step directly help deliver the requested outcome? -- Did I learn something that changes scope, or only implementation? -- Am I about to do more than the user asked? - -### After a user correction - -Treat the correction as a hard boundary update. - -Then: -- Remove the old broader plan. -- Do not defend the discarded work. -- Continue from the narrowed scope. -- If needed, acknowledge briefly and move on. - -## Decision Rules - -Use these rules in order: - -1. Prefer the most direct artifact first. - - Open the relevant file before scanning the whole repo. - - Inspect the specific failing path before designing a general framework. - -2. Prefer the smallest complete fix. - - Solve the asked problem before improving related systems. - - Avoid bonus work unless it is required for correctness. - -3. Prefer internal correction over user interruption. - - If you can shrink back to scope confidently, do it. - - Ask only when the next step changes deliverables, risk, or ownership. - -4. Treat repeated user constraints as priority signals. - - A repeated instruction means your execution style is currently misaligned. - - Tighten scope immediately. - -5. Separate categories of work. - - Code change, investigation, production remediation, cleanup, and documentation are distinct tasks unless the user explicitly combines them. - -## Good Intervention Style - -When you must pause, keep it short and specific: - -- State the potential drift in one sentence. -- Name the tradeoff or boundary. -- Offer the smallest on-scope option first. - -Example: - -"Quick alignment check: I can keep this to the code fix only, or also add an ops cleanup step. I'll stick to the code fix unless you want both." - -## Anti-Patterns - -Do not: - -- Create cleanup scripts, docs, or side tools just because they seem useful. -- Broaden the task after discovering a neighboring problem. -- Continue with a plan the user has already rejected. -- Justify drift with "best practice" when the user asked for a narrower deliverable. -- Hide extra work inside a larger patch. - -## Final Check Before Responding - -Before sending the final answer, verify: - -- The delivered work matches the requested outcome. -- No extra deliverables were added without confirmation. -- Any assumptions are stated briefly. -- Suggested next steps are optional, not bundled into the completed work. diff --git a/templates/skills/plan-and-execute.md b/templates/skills/plan-and-execute.md deleted file mode 100644 index 9fc8bd2d..00000000 --- a/templates/skills/plan-and-execute.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -name: plan-and-execute -description: Automatically plan and execute requirements. Creates a markdown task list with the UpdatePlan tool, and systematically executes each task while updating progress. Use when working with task planning or when you need to break down and execute complex multi-step requirements. ---- - -# Plan and Execute - -This Skill helps you automatically plan and execute requirements. It creates a structured markdown task list with the UpdatePlan tool and systematically works through each task while keeping progress visible. - -## Quick Start - -When you need to work through a multi-step request: - -1. Analyze the requirements and explore enough project context -2. Clarify unclear or ambiguous requirements with AskUserQuestion -3. Create a markdown task list by calling the UpdatePlan tool -4. Execute tasks one by one, updating the tool plan in real time -5. Revise the remaining plan as new context appears - -## Instructions - -### Step 1: Analyze the requirements - -Identify the requirements from the available context. Explore the project enough to make the plan concrete and accurate. - -If the original requirements are unclear, incomplete, or ambiguous, call the AskUserQuestion tool before creating the task list. Ask only the questions needed to avoid implementing the wrong behavior, and keep each question specific to the decision that affects the plan or acceptance criteria. - -If a required referenced file path is missing, ask for it with AskUserQuestion: - -``` -What is the path to the referenced file? -``` - -Referenced files can be in any text format (.md, .txt, etc.) that contains task requirements or feature descriptions. If no additional file is needed, continue from the available requirements. - -- What are the main requirements? -- What tasks need to be completed? -- Are there dependencies between tasks? -- What is the complexity level? -- Which files, modules, commands, or tests are relevant? -- What ambiguity would change the implementation or acceptance criteria? - -### Step 2: Create the task list - -Create a structured markdown task list and pass it to the UpdatePlan tool as the `plan` string. The tool input must use this shape: - -```json -{ - "plan": "## Task List\n\n- [ ] Task 1 description\n- [ ] Task 2 description\n- [ ] Task 3 description" -} -``` - -Use this markdown format for the `plan` content: - -```markdown -## Task List - -- [ ] Task 1 description -- [ ] Task 2 description -- [ ] Task 3 description -``` - -Break down complex requirements into specific, actionable tasks and call UpdatePlan with the full markdown task list. - -### Step 3: Execute tasks systematically - -For each task in the list: - -1. **Refresh the plan**: Before starting the first task and after completing each task, re-evaluate the latest conversation and project context. Update the remaining tasks when scope, order, blockers, or follow-up work changes. -2. **Mark as in progress**: Call UpdatePlan with the task changed from `[ ]` to `[>]` -3. **Execute the task**: Use appropriate tools to complete the work -4. **Mark as completed**: Call UpdatePlan with the task changed from `[>]` to `[x]` when finished -5. **Move to next task**: Only ONE task should be in progress at a time - -Important rules: -- Always keep the plan aligned with the latest context before executing the next task -- Always call UpdatePlan BEFORE starting work on a task -- Always call UpdatePlan IMMEDIATELY after completing a task -- Always pass the complete current markdown task list, not a partial diff -- Never work on multiple tasks simultaneously -- Remove tasks that are no longer relevant, and add newly discovered tasks before working on them -- If you encounter errors, keep the task as `[>]` and create new tasks to resolve blockers - -### Step 4: Handle task breakdown - -If during execution you discover a task is more complex than expected: - -1. Keep the current task as `[>]` -2. Call UpdatePlan with new sub-tasks below it with indentation: - ```markdown - - [>] Main task - - [ ] Sub-task 1 - - [ ] Sub-task 2 - ``` -3. Complete sub-tasks first, then mark the main task as complete with UpdatePlan - -### Step 5: Final verification - -After all tasks are completed (`[x]`): - -1. Review the original requirements to ensure everything is addressed -2. Run any final checks (tests, builds, linting) -3. Call UpdatePlan with every task marked `[x]` -4. Provide a concise completion summary in the final response - -## Task State Symbols - -- `[ ]` - Pending -- `[>]` - In progress -- `[x]` - Completed -- `[!]` - Blocked - -## Examples - -### Example 1: Simple feature request - -**Example requirements:** -```markdown -# 新功能:添加深色模式切换 - -用户应该能够在浅色和深色主题之间切换。 -切换开关应放在设置页面中。 -``` - -**分析后的 UpdatePlan 调用:** -```markdown -## Task List - -- [ ] 在设置页面创建深色模式切换组件 -- [ ] 添加深色模式状态管理(context/store) -- [ ] 实现深色主题的 CSS-in-JS 样式 -- [ ] 更新现有组件以支持主题切换 -- [ ] 运行测试并验证功能 -``` - -**UpdatePlan call during execution:** -```markdown -## Task List - -- [x] 在设置页面创建深色模式切换组件 -- [>] 添加深色模式状态管理(context/store) -- [ ] 实现深色主题的 CSS-in-JS 样式 -- [ ] 更新现有组件以支持主题切换 -- [ ] 运行测试并验证功能 -``` - -### Example 2: Bug fix with investigation - -**Example requirements:** -```markdown -# Fix bug:登录表单提交时崩溃 - -当用户点击提交时,应用崩溃。 -错误信息:"Cannot read property 'email' of undefined" -``` - -**UpdatePlan call after analysis:** -```markdown -## Task List - -- [ ] 在本地复现缺陷 -- [ ] 调查登录表单组件中的错误 -- [ ] 定位 undefined email 属性的根本原因 -- [ ] 实施修复 -- [ ] 添加验证以防止类似问题 -- [ ] 使用各种输入测试修复 -- [ ] 更新错误处理 -``` - -## When to Use This Skill - -Use this Skill when: - -1. **Complex multi-step tasks** - Request requires 3+ distinct steps -2. **Feature implementation** - Building new functionality from requirements -3. **Bug fixing** - Need to investigate, fix, and verify -4. **Refactoring** - Multiple files or components need changes -5. **Detailed requirements** - Specifications need to be translated into concrete tasks -6. **Need progress tracking** - Want visible progress without editing source files - -## When NOT to Use This Skill - -Skip this Skill when: - -1. **Single simple task** - Just one straightforward action needed -2. **Trivial changes** - Quick fixes that don't need planning -3. **Informational requests** - User just wants explanation, not execution -4. **No execution requested** - User only wants brainstorming or a high-level explanation - -## Best Practices - -1. **Be specific with tasks**: "Add login button to navbar" not "Update UI" -2. **Keep tasks atomic**: Each task should be independently completable -3. **Update immediately**: Don't batch status updates, do them in real-time -4. **One task at a time**: Never mark multiple tasks as `[>]` -5. **Handle blockers**: If stuck, create new tasks to resolve the blocker -6. **Verify completion**: Only mark `[x]` when task is fully done - -## Advanced Usage - -### Handling dependencies - -When tasks have dependencies, order them properly: - -```markdown -- [ ] Create database schema -- [ ] Implement API endpoints (depends on schema) -- [ ] Build frontend forms (depends on API) -``` - -### Using sub-tasks - -For complex tasks, break them down: - -```markdown -- [>] Implement authentication system - - [x] Set up JWT library - - [>] Create login endpoint - - [ ] Create logout endpoint - - [ ] Add token refresh logic -``` - -### Adding notes - -Add implementation notes or findings: - -```markdown -- [x] Investigate performance issue - - Note: Found N+1 query in user loader - - Solution: Added dataloader batching -``` - -## Workflow Summary - -1. Analyze the requirements and relevant project context -2. Call AskUserQuestion if the original requirements are unclear or ambiguous -3. Call UpdatePlan with the structured markdown task list -4. Refresh the remaining plan before the first task -5. For each task: - - Update to `[>]` with UpdatePlan - - Execute the task - - Update to `[x]` with UpdatePlan - - Re-evaluate and revise remaining tasks before moving on -6. Call UpdatePlan with all tasks completed and summarize the result - -This approach keeps planning and progress tracking in the UpdatePlan display, leaving source materials unchanged unless the actual task requires editing them. From b1a1d4a08626022b7678fb3dbec94abe2d7c9b7a Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 15 Jun 2026 10:50:45 +0800 Subject: [PATCH 160/212] 0.1.30 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09db34ab..fc94a3f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.29", + "version": "0.1.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.29", + "version": "0.1.30", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index aa925b92..add0e271 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.29", + "version": "0.1.30", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From cc7b0c31d6f156d7fb784e24a42f88785269e663 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 15 Jun 2026 10:58:18 +0800 Subject: [PATCH 161/212] feat: update templates/skills/bundled/deepcode-self-refer/SKILL.md --- templates/skills/bundled/deepcode-self-refer/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/skills/bundled/deepcode-self-refer/SKILL.md b/templates/skills/bundled/deepcode-self-refer/SKILL.md index 56b902ef..357868cd 100644 --- a/templates/skills/bundled/deepcode-self-refer/SKILL.md +++ b/templates/skills/bundled/deepcode-self-refer/SKILL.md @@ -1,6 +1,6 @@ --- name: deepcode-self-refer -description: 回答关于 Deep Code CLI 本身的问题——包括功能特性、配置项、斜杠命令、Skills、MCP 集成、权限、通知、会话持久化及故障排查。当用户询问如何配置或使用 Deep Code、如何设置 MCP 服务器、配置通知(如 Slack/飞书)、管理权限、查看可用技能、理解斜杠命令、配置思考模式、使用 Undo 功能,或咨询 Deep Code 与 VSCode 集成等场景时使用。 +description: Answers questions about Deep Code CLI itself — including features, configuration options, slash commands, Skills, MCP integration, permissions, notifications, session persistence, and troubleshooting. Use this when users ask how to configure or use Deep Code, how to set up an MCP server, configure notifications (such as Slack/Feishu), manage permissions, view available skills, understand slash commands, configure thinking mode, etc. --- # Deep Code Self-Refer From f725737e0bc5f6029f7f6badcf6186607113d023 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 16 Jun 2026 13:11:50 +0800 Subject: [PATCH 162/212] feat: update bundled/skill-digester/SKILL.md --- .../skills/bundled/skill-digester/SKILL.md | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/templates/skills/bundled/skill-digester/SKILL.md b/templates/skills/bundled/skill-digester/SKILL.md index 6e9016d1..acc969e8 100644 --- a/templates/skills/bundled/skill-digester/SKILL.md +++ b/templates/skills/bundled/skill-digester/SKILL.md @@ -1,18 +1,28 @@ --- name: skill-digester -description: Reviews and improves another DeepCode skill's SKILL.md description field against the Agent Skills description-field rules. Use when the user asks to "digest" a skill, including requests like "digest the pdf skill" or "消化 pdf 技能". +description: Reviews and improves another DeepCode skill's SKILL.md description field, and guides Agent Skill installation into user or project .agents/skills roots. Use when the user asks to digest a skill, install an Agent Skill, install a skill to user/project scope, or says "消化技能" or "安装 agent skill". --- # Skill Digester -Use this skill to review and optionally rewrite the `description` field of another DeepCode skill. +Use this skill for two related tasks: + +- Review and optionally rewrite the `description` field of another DeepCode skill. +- Guide installation of an Agent Skill into an interoperable `.agents/skills` root. ## Interaction Rule -Whenever user input is needed, call the `AskUserQuestion` tool. Do not ask follow-up questions as plain assistant text. This includes missing skill names, language preference, duplicate matches, malformed frontmatter decisions, and whether to apply a recommended rewrite. +Whenever user input is needed, call the `AskUserQuestion` tool. Do not ask follow-up questions as plain assistant text. This includes missing skill names or paths, install scope, language preference, duplicate matches, malformed frontmatter decisions, and whether to apply a recommended rewrite. ## Workflow +First classify the request: + +- If the user asks to install, add, copy, or place an Agent Skill, use the [Install Agent Skill Workflow](#install-agent-skill-workflow). +- Otherwise, use the [Digest Description Workflow](#digest-description-workflow). + +## Digest Description Workflow + 1. Identify the target skill from the user's request. - If the user did not provide a skill name, use `AskUserQuestion` to ask for one. - Locate the skill by running the bundled Node script from this skill directory: @@ -82,6 +92,46 @@ Whenever user input is needed, call the `AskUserQuestion` tool. Do not ask follo - Preserve body content exactly unless the user separately asks to edit it. - After editing, report the source path, updated digest output path, and final description. +## Install Agent Skill Workflow + +Use this workflow when the user asks to install an Agent Skill. Installation always writes to `.agents/skills`, not `.deepcode/skills`. + +1. Identify the source skill directory. + - If the user provided an explicit file or directory path, resolve it: + - `~/...` relative to the user's home directory. + - `./...` relative to the current project root. + - Absolute paths as written. + - A `SKILL.md` path means its parent directory is the source skill directory. + - If the user provided a skill name instead of a path, locate it with `scripts/find-skill.js` using the same command and match rules as the digest workflow. + - If the user did not provide a skill name or path, use `AskUserQuestion` to ask for the source skill name or path. + - The source directory must contain `SKILL.md`. If it does not, report that the path is not an Agent Skill and ask for another source only if the user still wants to install. + +2. Determine the installed skill folder name. + - Parse the source `SKILL.md` frontmatter. + - Use the trimmed frontmatter `name` when present. + - Otherwise use the source folder name with underscores converted to hyphens. + - Use that resolved name as the target folder name. + +3. Ask exactly one installation scope question. + - Use `AskUserQuestion` to ask whether to install the skill at user level or project level. + - Offer only these scope choices: + - User-level install: `~/.agents/skills//` + - Project-level install: `./.agents/skills//` + - Do not ask any other installation preference before copying. + +4. Copy the complete skill directory. + - User-level destination: `~/.agents/skills//`. + - Project-level destination: `./.agents/skills//`. + - Copy the whole source skill directory, including `SKILL.md`, `references/`, `scripts/`, `templates/`, examples, assets, and other support files. + - Preserve file contents and relative paths exactly. + - Create the `.agents/skills` parent directory if needed. + - If the destination directory already exists, stop and report the conflict. Do not overwrite or merge files unless the user explicitly asks in a later message. + +5. Report the result. + - Report the source directory and installation destination. + - Mention that the agent client may need to reload or restart before the installed skill appears. + - Do not digest, rewrite, or normalize the installed skill unless the user separately asks for that. + ## AskUserQuestion Patterns Use one question at a time unless two decisions are tightly coupled. Each question must include `options`; rely on the UI's `Other` option for free-form input. @@ -96,6 +146,10 @@ Examples: {"questions":[{"question":"How should I proceed with this description recommendation?","options":[{"label":"Apply change","description":"Update only the description field in the native digest output SKILL.md."},{"label":"Abandon change","description":"Leave the file unchanged."},{"label":"Discuss wording","description":"Continue refining the proposed description before editing."}]}]} ``` +```json +{"questions":[{"question":"Where should I install this Agent Skill?","options":[{"label":"User-level","description":"Install to ~/.agents/skills so it is available across projects."},{"label":"Project-level","description":"Install to ./.agents/skills so it is available in this project."}]}]} +``` + ## Review Heuristics A strong description is short, concrete, and activation-oriented. Prefer this pattern: @@ -110,6 +164,7 @@ Avoid descriptions that are only generic labels, marketing copy, or internal imp - Never modify a different skill with a similar name without asking. - Never save the digested output under `.agents/skills`; `.agents/skills` is only a source root for digestion. +- Never save installed Agent Skills under `.deepcode/skills`; installation writes only to `.agents/skills`. - Never move a skill between project and user level during digestion. +- Never overwrite or merge an existing installed skill directory unless the user explicitly asks after seeing the conflict. - Never change the target skill's language preference after confirmation unless the user asks. - From f11ab92a9dc29ec81b4222a4e6d194f8f0546dfc Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 16 Jun 2026 13:18:55 +0800 Subject: [PATCH 163/212] 0.1.31 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c64d6e43..8db59638 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.30", + "version": "0.1.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vegamo/deepcode-cli", - "version": "0.1.30", + "version": "0.1.31", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index add0e271..d1165f4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.30", + "version": "0.1.31", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", From 2b8dc93a0864adf07b8e9f7caf1071a1f2ac8c3b Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 16 Jun 2026 14:23:07 +0800 Subject: [PATCH 164/212] fix: update ESLint config for browser globals and fix vscode extension build --- .github/workflows/ci.yml | 9 +- .gitignore | 3 + eslint.config.mjs | 35 +- package-lock.json | 7739 ++++++++++++----- package.json | 64 +- packages/cli/package.json | 42 + {src => packages/cli/src}/cli.tsx | 2 +- .../cli/src}/common/update-check.ts | 2 +- packages/cli/src/generated/git-commit.ts | 8 + .../cli/src}/tests/ask-user-question.test.ts | 2 +- .../cli/src}/tests/clipboard.test.ts | 0 .../cli/src}/tests/dropdown-menu.test.ts | 0 .../cli/src}/tests/exit-summary.test.ts | 2 +- .../cli/src}/tests/file-mentions.test.ts | 0 .../cli/src}/tests/loading-text.test.ts | 0 .../cli/src}/tests/markdown.test.ts | 0 .../cli/src}/tests/message-view.test.ts | 2 +- .../cli/src}/tests/permission-prompt.test.ts | 0 .../cli/src}/tests/prompt-buffer.test.ts | 0 .../cli/src}/tests/prompt-input-keys.test.ts | 2 +- .../cli/src}/tests/prompt-undo-redo.test.ts | 0 packages/cli/src/tests/run-tests.mjs | 15 + .../cli/src}/tests/session-list.test.ts | 2 +- .../cli/src}/tests/slash-commands.test.ts | 2 +- .../cli/src}/tests/thinking-state.test.ts | 2 +- .../cli/src}/tests/update-check.test.ts | 0 .../cli/src}/tests/welcome-screen.test.ts | 0 {src => packages/cli/src}/ui/ascii-art.ts | 0 .../src}/ui/components/DropdownMenu/index.tsx | 0 .../ui/components/FileMentionMenu/index.tsx | 0 .../src}/ui/components/MessageView/index.tsx | 0 .../ui/components/MessageView/markdown.ts | 0 .../src}/ui/components/MessageView/types.ts | 2 +- .../src}/ui/components/MessageView/utils.ts | 2 +- .../ui/components/ModelsDropdown/index.tsx | 2 +- .../ui/components/RawModeExitPrompt/index.tsx | 0 .../ui/components/RawModelDropdown/index.tsx | 0 .../ui/components/SkillsDropdown/index.tsx | 2 +- .../cli/src}/ui/components/index.ts | 0 {src => packages/cli/src}/ui/constants.ts | 0 .../cli/src}/ui/contexts/AppContext.tsx | 0 .../cli/src}/ui/contexts/RawModeContext.tsx | 0 .../cli/src}/ui/contexts/index.ts | 0 .../cli/src}/ui/core/ask-user-question.ts | 2 +- .../cli/src}/ui/core/clipboard.ts | 0 .../cli/src}/ui/core/file-mentions.ts | 0 .../cli/src}/ui/core/loading-text.ts | 2 +- .../cli/src}/ui/core/prompt-buffer.ts | 0 .../cli/src}/ui/core/prompt-undo-redo.ts | 0 .../cli/src}/ui/core/slash-commands.ts | 2 +- .../cli/src}/ui/core/thinking-state.ts | 2 +- {src => packages/cli/src}/ui/exit-summary.ts | 2 +- {src => packages/cli/src}/ui/hooks/cursor.ts | 0 {src => packages/cli/src}/ui/hooks/index.ts | 0 .../cli/src}/ui/hooks/useHistoryNavigation.ts | 0 .../cli/src}/ui/hooks/usePasteHandling.ts | 0 .../cli/src}/ui/hooks/useTerminalInput.ts | 0 {src => packages/cli/src}/ui/index.ts | 0 {src => packages/cli/src}/ui/utils/index.ts | 6 +- {src => packages/cli/src}/ui/views/App.tsx | 12 +- .../cli/src}/ui/views/AppContainer.tsx | 0 .../src}/ui/views/AskUserQuestionPrompt.tsx | 0 .../cli/src}/ui/views/McpStatusList.tsx | 2 +- .../cli/src}/ui/views/PermissionPrompt.tsx | 4 +- .../cli/src}/ui/views/ProcessStdoutView.tsx | 4 +- .../cli/src}/ui/views/PromptInput.tsx | 6 +- .../cli/src}/ui/views/SessionList.tsx | 2 +- .../cli/src}/ui/views/SlashCommandMenu.tsx | 2 +- .../cli/src}/ui/views/ThemedGradient.tsx | 0 .../cli/src}/ui/views/UndoSelector.tsx | 2 +- .../cli/src}/ui/views/UpdatePrompt.tsx | 0 .../cli/src}/ui/views/WelcomeScreen.tsx | 4 +- packages/cli/tsconfig.json | 26 + packages/core/package.json | 39 + .../core/src}/common/bash-timeout.ts | 0 .../core/src}/common/debug-logger.ts | 0 .../core/src}/common/error-logger.ts | 0 .../core/src}/common/file-history.ts | 0 .../core/src}/common/file-utils.ts | 0 .../core/src}/common/model-capabilities.ts | 0 {src => packages/core/src}/common/notify.ts | 0 .../core/src}/common/openai-client.ts | 0 .../src}/common/openai-message-converter.ts | 0 .../core/src}/common/openai-thinking.ts | 0 .../core/src}/common/permissions.ts | 0 .../core/src}/common/process-tree.ts | 0 .../core/src}/common/shell-utils.ts | 0 {src => packages/core/src}/common/state.ts | 0 .../core/src}/common/telemetry.ts | 0 packages/core/src/common/tool-types.ts | 107 + {src => packages/core/src}/common/validate.ts | 2 +- packages/core/src/generated/git-commit.ts | 8 + packages/core/src/index.ts | 132 + {src => packages/core/src}/mcp/mcp-client.ts | 0 {src => packages/core/src}/mcp/mcp-manager.ts | 0 {src => packages/core/src}/prompt.ts | 0 {src => packages/core/src}/session.ts | 0 {src => packages/core/src}/settings.ts | 0 .../core/src}/tests/debug-logger.test.ts | 0 .../core/src}/tests/mcp-client.test.ts | 0 .../core/src}/tests/memory-leak.test.ts | 0 .../tests/openai-message-converter.test.ts | 0 .../core/src}/tests/openai-thinking.test.ts | 0 .../core/src}/tests/permissions.test.ts | 0 .../core/src}/tests/process-tree.test.ts | 0 .../core/src}/tests/prompt.test.ts | 0 packages/core/src/tests/run-tests.mjs | 15 + .../core/src}/tests/session.test.ts | 0 .../src}/tests/settings-and-notify.test.ts | 0 .../core/src}/tests/shell-utils.test.ts | 0 .../core/src}/tests/telemetry.test.ts | 0 .../core/src}/tests/tool-executor.test.ts | 0 .../core/src}/tests/tool-handlers.test.ts | 0 .../src}/tests/web-search-handler.test.ts | 0 .../src}/tools/ask-user-question-handler.ts | 0 .../core/src}/tools/bash-handler.ts | 0 .../core/src}/tools/edit-handler.ts | 0 {src => packages/core/src}/tools/executor.ts | 130 +- .../core/src}/tools/read-handler.ts | 0 .../core/src}/tools/update-plan-handler.ts | 0 .../core/src}/tools/web-search-handler.ts | 0 .../core/src}/tools/write-handler.ts | 0 .../templates}/prompts/init_command.md.ejs | 0 .../bundled/deepcode-self-refer/SKILL.md | 20 +- .../deepcode-self-refer/references/README.md | 49 +- .../references/configuration.md | 100 +- .../references/configuration_en.md | 97 +- .../deepcode-self-refer/references/mcp.md | 0 .../deepcode-self-refer/references/mcp_en.md | 22 +- .../deepcode-self-refer/references/notify.md | 14 +- .../references/notify_en.md | 14 +- .../references/permission.md | 34 +- .../references/permission_en.md | 33 +- .../references/session-persistence.md | 40 +- .../references/session-persistence_en.md | 40 +- .../templates}/skills/bundled/plan/SKILL.md | 72 +- .../skills/bundled/skill-digester/SKILL.md | 30 +- .../skill-digester/scripts/find-skill.js | 0 .../skills/bundled/skill-writer/SKILL.md | 29 +- .../templates}/skills/karpathy-guidelines.md | 7 +- .../templates}/tools/ask-user-question.md | 2 + .../core/templates}/tools/bash.md | 65 +- .../core/templates}/tools/edit.md | 7 +- .../core/templates}/tools/read.md.ejs | 0 .../core/templates}/tools/update-plan.md | 5 +- .../core/templates}/tools/web-search.md | 2 + .../core/templates}/tools/write.md | 8 +- packages/core/tsconfig.json | 21 + packages/core/tsconfig.tsbuildinfo | 1 + packages/vscode-ide-companion/.vscodeignore | 8 + packages/vscode-ide-companion/LICENSE | 21 + packages/vscode-ide-companion/README.md | 94 + packages/vscode-ide-companion/README_cn.md | 94 + packages/vscode-ide-companion/README_en.md | 87 + packages/vscode-ide-companion/package.json | 85 + .../resources/deepcode_screenshot.png | Bin 0 -> 357519 bytes .../resources/deepcoding_icon.png | Bin 0 -> 77727 bytes .../resources/deepcoding_icon.svg | 1 + .../vscode-ide-companion/resources/faq1.gif | Bin 0 -> 186246 bytes .../resources/prompt-attachments.js | 273 + .../resources/webview.css | 1604 ++++ .../resources/webview.html | 2354 +++++ .../vscode-ide-companion/src/extension.ts | 562 ++ packages/vscode-ide-companion/src/provider.ts | 319 + .../src/tests/extension-utils.test.ts | 132 + .../src/tests/extension.test.ts | 445 + .../src/tests/run-tests.mjs | 15 + packages/vscode-ide-companion/src/utils.ts | 61 + .../vscode-ide-companion/tsconfig.build.json | 21 + packages/vscode-ide-companion/tsconfig.json | 23 + scripts/build-vscode-companion.js | 23 + scripts/build.js | 23 + scripts/clean.js | 39 + ...bundle_assets.js => copy-bundle-assets.js} | 14 +- scripts/esbuild-vscode.config.js | 29 + scripts/esbuild.config.js | 28 + scripts/generate-git-commit-info.js | 46 + scripts/start.js | 22 + src/tests/run-tests.mjs | 13 - tsconfig.json | 39 +- 180 files changed, 12838 insertions(+), 2803 deletions(-) create mode 100644 packages/cli/package.json rename {src => packages/cli/src}/cli.tsx (98%) rename {src => packages/cli/src}/common/update-check.ts (99%) create mode 100644 packages/cli/src/generated/git-commit.ts rename {src => packages/cli/src}/tests/ask-user-question.test.ts (98%) rename {src => packages/cli/src}/tests/clipboard.test.ts (100%) rename {src => packages/cli/src}/tests/dropdown-menu.test.ts (100%) rename {src => packages/cli/src}/tests/exit-summary.test.ts (97%) rename {src => packages/cli/src}/tests/file-mentions.test.ts (100%) rename {src => packages/cli/src}/tests/loading-text.test.ts (100%) rename {src => packages/cli/src}/tests/markdown.test.ts (100%) rename {src => packages/cli/src}/tests/message-view.test.ts (99%) rename {src => packages/cli/src}/tests/permission-prompt.test.ts (100%) rename {src => packages/cli/src}/tests/prompt-buffer.test.ts (100%) rename {src => packages/cli/src}/tests/prompt-input-keys.test.ts (99%) rename {src => packages/cli/src}/tests/prompt-undo-redo.test.ts (100%) create mode 100644 packages/cli/src/tests/run-tests.mjs rename {src => packages/cli/src}/tests/session-list.test.ts (98%) rename {src => packages/cli/src}/tests/slash-commands.test.ts (98%) rename {src => packages/cli/src}/tests/thinking-state.test.ts (96%) rename {src => packages/cli/src}/tests/update-check.test.ts (100%) rename {src => packages/cli/src}/tests/welcome-screen.test.ts (100%) rename {src => packages/cli/src}/ui/ascii-art.ts (100%) rename {src => packages/cli/src}/ui/components/DropdownMenu/index.tsx (100%) rename {src => packages/cli/src}/ui/components/FileMentionMenu/index.tsx (100%) rename {src => packages/cli/src}/ui/components/MessageView/index.tsx (100%) rename {src => packages/cli/src}/ui/components/MessageView/markdown.ts (100%) rename {src => packages/cli/src}/ui/components/MessageView/types.ts (84%) rename {src => packages/cli/src}/ui/components/MessageView/utils.ts (99%) rename {src => packages/cli/src}/ui/components/ModelsDropdown/index.tsx (98%) rename {src => packages/cli/src}/ui/components/RawModeExitPrompt/index.tsx (100%) rename {src => packages/cli/src}/ui/components/RawModelDropdown/index.tsx (100%) rename {src => packages/cli/src}/ui/components/SkillsDropdown/index.tsx (97%) rename {src => packages/cli/src}/ui/components/index.ts (100%) rename {src => packages/cli/src}/ui/constants.ts (100%) rename {src => packages/cli/src}/ui/contexts/AppContext.tsx (100%) rename {src => packages/cli/src}/ui/contexts/RawModeContext.tsx (100%) rename {src => packages/cli/src}/ui/contexts/index.ts (100%) rename {src => packages/cli/src}/ui/core/ask-user-question.ts (98%) rename {src => packages/cli/src}/ui/core/clipboard.ts (100%) rename {src => packages/cli/src}/ui/core/file-mentions.ts (100%) rename {src => packages/cli/src}/ui/core/loading-text.ts (96%) rename {src => packages/cli/src}/ui/core/prompt-buffer.ts (100%) rename {src => packages/cli/src}/ui/core/prompt-undo-redo.ts (100%) rename {src => packages/cli/src}/ui/core/slash-commands.ts (98%) rename {src => packages/cli/src}/ui/core/thinking-state.ts (94%) rename {src => packages/cli/src}/ui/exit-summary.ts (98%) rename {src => packages/cli/src}/ui/hooks/cursor.ts (100%) rename {src => packages/cli/src}/ui/hooks/index.ts (100%) rename {src => packages/cli/src}/ui/hooks/useHistoryNavigation.ts (100%) rename {src => packages/cli/src}/ui/hooks/usePasteHandling.ts (100%) rename {src => packages/cli/src}/ui/hooks/useTerminalInput.ts (100%) rename {src => packages/cli/src}/ui/index.ts (100%) rename {src => packages/cli/src}/ui/utils/index.ts (94%) rename {src => packages/cli/src}/ui/views/App.tsx (98%) rename {src => packages/cli/src}/ui/views/AppContainer.tsx (100%) rename {src => packages/cli/src}/ui/views/AskUserQuestionPrompt.tsx (100%) rename {src => packages/cli/src}/ui/views/McpStatusList.tsx (99%) rename {src => packages/cli/src}/ui/views/PermissionPrompt.tsx (98%) rename {src => packages/cli/src}/ui/views/ProcessStdoutView.tsx (98%) rename {src => packages/cli/src}/ui/views/PromptInput.tsx (99%) rename {src => packages/cli/src}/ui/views/SessionList.tsx (99%) rename {src => packages/cli/src}/ui/views/SlashCommandMenu.tsx (98%) rename {src => packages/cli/src}/ui/views/ThemedGradient.tsx (100%) rename {src => packages/cli/src}/ui/views/UndoSelector.tsx (99%) rename {src => packages/cli/src}/ui/views/UpdatePrompt.tsx (100%) rename {src => packages/cli/src}/ui/views/WelcomeScreen.tsx (97%) create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/core/package.json rename {src => packages/core/src}/common/bash-timeout.ts (100%) rename {src => packages/core/src}/common/debug-logger.ts (100%) rename {src => packages/core/src}/common/error-logger.ts (100%) rename {src => packages/core/src}/common/file-history.ts (100%) rename {src => packages/core/src}/common/file-utils.ts (100%) rename {src => packages/core/src}/common/model-capabilities.ts (100%) rename {src => packages/core/src}/common/notify.ts (100%) rename {src => packages/core/src}/common/openai-client.ts (100%) rename {src => packages/core/src}/common/openai-message-converter.ts (100%) rename {src => packages/core/src}/common/openai-thinking.ts (100%) rename {src => packages/core/src}/common/permissions.ts (100%) rename {src => packages/core/src}/common/process-tree.ts (100%) rename {src => packages/core/src}/common/shell-utils.ts (100%) rename {src => packages/core/src}/common/state.ts (100%) rename {src => packages/core/src}/common/telemetry.ts (100%) create mode 100644 packages/core/src/common/tool-types.ts rename {src => packages/core/src}/common/validate.ts (99%) create mode 100644 packages/core/src/generated/git-commit.ts create mode 100644 packages/core/src/index.ts rename {src => packages/core/src}/mcp/mcp-client.ts (100%) rename {src => packages/core/src}/mcp/mcp-manager.ts (100%) rename {src => packages/core/src}/prompt.ts (100%) rename {src => packages/core/src}/session.ts (100%) rename {src => packages/core/src}/settings.ts (100%) rename {src => packages/core/src}/tests/debug-logger.test.ts (100%) rename {src => packages/core/src}/tests/mcp-client.test.ts (100%) rename {src => packages/core/src}/tests/memory-leak.test.ts (100%) rename {src => packages/core/src}/tests/openai-message-converter.test.ts (100%) rename {src => packages/core/src}/tests/openai-thinking.test.ts (100%) rename {src => packages/core/src}/tests/permissions.test.ts (100%) rename {src => packages/core/src}/tests/process-tree.test.ts (100%) rename {src => packages/core/src}/tests/prompt.test.ts (100%) create mode 100644 packages/core/src/tests/run-tests.mjs rename {src => packages/core/src}/tests/session.test.ts (100%) rename {src => packages/core/src}/tests/settings-and-notify.test.ts (100%) rename {src => packages/core/src}/tests/shell-utils.test.ts (100%) rename {src => packages/core/src}/tests/telemetry.test.ts (100%) rename {src => packages/core/src}/tests/tool-executor.test.ts (100%) rename {src => packages/core/src}/tests/tool-handlers.test.ts (100%) rename {src => packages/core/src}/tests/web-search-handler.test.ts (100%) rename {src => packages/core/src}/tools/ask-user-question-handler.ts (100%) rename {src => packages/core/src}/tools/bash-handler.ts (100%) rename {src => packages/core/src}/tools/edit-handler.ts (100%) rename {src => packages/core/src}/tools/executor.ts (67%) rename {src => packages/core/src}/tools/read-handler.ts (100%) rename {src => packages/core/src}/tools/update-plan-handler.ts (100%) rename {src => packages/core/src}/tools/web-search-handler.ts (100%) rename {src => packages/core/src}/tools/write-handler.ts (100%) rename {templates => packages/core/templates}/prompts/init_command.md.ejs (100%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/SKILL.md (81%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/README.md (87%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/configuration.md (64%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/configuration_en.md (71%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/mcp.md (100%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/mcp_en.md (96%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/notify.md (91%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/notify_en.md (91%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/permission.md (61%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/permission_en.md (64%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/session-persistence.md (75%) rename {templates => packages/core/templates}/skills/bundled/deepcode-self-refer/references/session-persistence_en.md (72%) rename {templates => packages/core/templates}/skills/bundled/plan/SKILL.md (78%) rename {templates => packages/core/templates}/skills/bundled/skill-digester/SKILL.md (89%) rename {templates => packages/core/templates}/skills/bundled/skill-digester/scripts/find-skill.js (100%) rename {templates => packages/core/templates}/skills/bundled/skill-writer/SKILL.md (99%) rename {templates => packages/core/templates}/skills/karpathy-guidelines.md (97%) rename {templates => packages/core/templates}/tools/ask-user-question.md (99%) rename {templates => packages/core/templates}/tools/bash.md (54%) rename {templates => packages/core/templates}/tools/edit.md (97%) rename {templates => packages/core/templates}/tools/read.md.ejs (100%) rename {templates => packages/core/templates}/tools/update-plan.md (97%) rename {templates => packages/core/templates}/tools/web-search.md (99%) rename {templates => packages/core/templates}/tools/write.md (86%) create mode 100644 packages/core/tsconfig.json create mode 100644 packages/core/tsconfig.tsbuildinfo create mode 100644 packages/vscode-ide-companion/.vscodeignore create mode 100644 packages/vscode-ide-companion/LICENSE create mode 100644 packages/vscode-ide-companion/README.md create mode 100644 packages/vscode-ide-companion/README_cn.md create mode 100644 packages/vscode-ide-companion/README_en.md create mode 100644 packages/vscode-ide-companion/package.json create mode 100644 packages/vscode-ide-companion/resources/deepcode_screenshot.png create mode 100644 packages/vscode-ide-companion/resources/deepcoding_icon.png create mode 100644 packages/vscode-ide-companion/resources/deepcoding_icon.svg create mode 100644 packages/vscode-ide-companion/resources/faq1.gif create mode 100644 packages/vscode-ide-companion/resources/prompt-attachments.js create mode 100644 packages/vscode-ide-companion/resources/webview.css create mode 100644 packages/vscode-ide-companion/resources/webview.html create mode 100644 packages/vscode-ide-companion/src/extension.ts create mode 100644 packages/vscode-ide-companion/src/provider.ts create mode 100644 packages/vscode-ide-companion/src/tests/extension-utils.test.ts create mode 100644 packages/vscode-ide-companion/src/tests/extension.test.ts create mode 100644 packages/vscode-ide-companion/src/tests/run-tests.mjs create mode 100644 packages/vscode-ide-companion/src/utils.ts create mode 100644 packages/vscode-ide-companion/tsconfig.build.json create mode 100644 packages/vscode-ide-companion/tsconfig.json create mode 100644 scripts/build-vscode-companion.js create mode 100644 scripts/build.js create mode 100644 scripts/clean.js rename scripts/{copy_bundle_assets.js => copy-bundle-assets.js} (62%) create mode 100644 scripts/esbuild-vscode.config.js create mode 100644 scripts/esbuild.config.js create mode 100644 scripts/generate-git-commit-info.js create mode 100644 scripts/start.js delete mode 100644 src/tests/run-tests.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dc891f0..db286bd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ jobs: - windows-latest - macos-latest node-version: - - "20" - "22" - "24" @@ -38,8 +37,14 @@ jobs: - name: TypeCheck + Lint + Format Check run: npm run check - - name: Bundle + - name: Build Core + run: npm run build --workspace=@vegamo/deepcode-core + + - name: Bundle CLI run: npm run bundle + - name: Build VSCode Extension + run: npm run build:vscode + - name: Test run: npm test diff --git a/.gitignore b/.gitignore index 8f054d4b..cd80bbf9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ node_modules/ dist/ +out/ +src/generated/ .DS_Store .idea/ .vscode/ *.tgz *.log +*.vsix .deepcode/settings.json diff --git a/eslint.config.mjs b/eslint.config.mjs index 50e41491..b9ec5dc1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -43,12 +43,43 @@ export default tseslint.config( }, // Test files: relaxed rules { - files: ["src/tests/**/*.ts"], + files: ["packages/*/src/tests/**/*.ts", "packages/*/src/tests/**/*.mjs"], + languageOptions: { + globals: { + process: "readonly", + console: "readonly", + }, + }, rules: { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "off", }, }, + // Script files: Node.js environment + { + files: ["./scripts/**/*.js", "./scripts/**/*.mjs", "packages/*/scripts/**/*.js"], + languageOptions: { + globals: { + process: "readonly", + console: "readonly", + }, + }, + }, + // Browser resources: VSCode webview scripts + { + files: ["packages/*/resources/**/*.js"], + languageOptions: { + globals: { + window: "readonly", + document: "readonly", + console: "readonly", + FileReader: "readonly", + Blob: "readonly", + URL: "readonly", + fetch: "readonly", + }, + }, + }, // Prettier config: disable conflicting ESLint rules, MUST be last - prettierConfig, + prettierConfig ); diff --git a/package-lock.json b/package-lock.json index fc94a3f8..3e6afcbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,14 @@ { - "name": "@vegamo/deepcode-cli", - "version": "0.1.30", + "name": "deepcode", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@vegamo/deepcode-cli", - "version": "0.1.30", + "name": "deepcode", "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "ejs": "^5.0.2", - "gradient-string": "^3.0.0", - "gray-matter": "^4.0.3", - "ignore": "^7.0.5", - "ink": "^7.0.4", - "ink-gradient": "^4.0.1", - "openai": "^6.35.0", - "react": "^19.2.5", - "undici": "^7.25.0", - "zod": "^4.4.3" - }, - "bin": { - "deepcode": "dist/cli.js" - }, + "workspaces": [ + "packages/*" + ], "devDependencies": { "@eslint/js": "^9.39.4", "@types/ejs": "^3.1.5", @@ -40,9 +25,6 @@ "tsx": "^4.21.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2" - }, - "engines": { - "node": ">=22" } }, "node_modules/@alcalzone/ansi-tokenize": { @@ -58,14 +40,211 @@ "node": ">=18" } }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.2", + "resolved": "https://registry.npmmirror.com/@azure/core-client/-/core-client-1.10.2.tgz", + "integrity": "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.24.0", + "resolved": "https://registry.npmmirror.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.24.0.tgz", + "integrity": "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmmirror.com/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmmirror.com/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.13.0", + "resolved": "https://registry.npmmirror.com/@azure/msal-browser/-/msal-browser-5.13.0.tgz", + "integrity": "sha512-Ea23x0U8XNFY+qJ9T44zO2BbY+AHdb+WdjmYnx36OhJ/KO+PGU5pmsNHf1DCElYX+6wyVRJz1HFeCPC/cHbRug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.8.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.8.0", + "resolved": "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-16.8.0.tgz", + "integrity": "sha512-5S4RHOcInL2Nu2U217tDZbWGI6StMfcWCrA7TWvWdJmXQ+cYrrIqr84AsN62fGh2MDBysiBJPt6CfWceJfloEA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@azure/msal-node/-/msal-node-5.2.4.tgz", + "integrity": "sha512-rpBUg9dA8UpC2WiFt3KeDKVQmmmVrfxdRnW+F1ebgou/jX/0tAvYuonaq5RUo8OaqzOrj4x/HaI8DmY56RXZ2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.8.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -74,9 +253,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -84,21 +263,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -115,14 +294,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -132,14 +311,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -149,9 +328,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -159,29 +338,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -191,9 +370,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -201,9 +380,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -211,9 +390,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -221,27 +400,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -251,33 +430,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -285,23 +464,23 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -316,9 +495,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -333,9 +512,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -350,9 +529,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -367,9 +546,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -384,9 +563,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -401,9 +580,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -418,9 +597,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -435,9 +614,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -452,9 +631,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -469,9 +648,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -486,9 +665,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -503,9 +682,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -520,9 +699,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -537,9 +716,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -554,9 +733,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -571,9 +750,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -588,9 +767,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -605,9 +784,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -622,9 +801,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -639,9 +818,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -656,9 +835,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -673,9 +852,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -690,9 +869,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -707,9 +886,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -724,9 +903,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -847,36 +1026,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@eslint/js": { "version": "9.39.4", "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.4.tgz", @@ -1030,1149 +1179,4054 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@types/ejs": { - "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/@types/ejs/-/ejs-3.1.5.tgz", - "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, - "license": "MIT" - }, - "node_modules/@types/gradient-string": { - "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/@types/gradient-string/-/gradient-string-1.1.6.tgz", - "integrity": "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==", "license": "MIT", "dependencies": { - "@types/tinycolor2": "*" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 8" + } }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.2.2" + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@types/tinycolor2": { - "version": "1.4.6", - "resolved": "https://registry.npmmirror.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", - "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.2", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "node_modules/@secretlint/config-loader/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "node_modules/@secretlint/config-loader/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", - "debug": "^4.4.3" + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } + "license": "MIT" }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node-sarif-builder": "^3.2.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", "dev": true, "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.5" + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=20.0.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "node_modules/@textlint/ast-node-types": { + "version": "15.7.1", + "resolved": "https://registry.npmmirror.com/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.7.1", + "resolved": "https://registry.npmmirror.com/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.7.1", + "@textlint/resolver": "15.7.1", + "@textlint/types": "15.7.1", + "chalk": "^4.1.2", + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=8" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@textlint/linter-formatter/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } + "license": "MIT" }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/@textlint/linter-formatter/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=8" } }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { - "environment": "^1.0.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } + "node_modules/@textlint/module-interop": { + "version": "15.7.1", + "resolved": "https://registry.npmmirror.com/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", + "dev": true, + "license": "MIT" }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@textlint/resolver": { + "version": "15.7.1", + "resolved": "https://registry.npmmirror.com/@textlint/resolver/-/resolver-15.7.1.tgz", + "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.7.1", + "resolved": "https://registry.npmmirror.com/@textlint/types/-/types-15.7.1.tgz", + "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "dependencies": { + "@textlint/ast-node-types": "15.7.1" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/gradient-string": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@types/gradient-string/-/gradient-string-1.1.6.tgz", + "integrity": "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==", "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" + "@types/tinycolor2": "*" } }, - "node_modules/auto-bind": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/auto-bind/-/auto-bind-5.0.1.tgz", - "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "dev": true, "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.29", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", - "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" } }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmmirror.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "devOptional": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "csstype": "^3.2.2" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmmirror.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.120.0", + "resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.120.0.tgz", + "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 4" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001792", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", - "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/cli-boxes": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-4.0.1.tgz", - "integrity": "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "debug": "^4.4.3" + }, "engines": { - "node": ">=18.20 <19 || >=20.10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^4.0.0" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/cli-truncate": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-6.0.0.tgz", - "integrity": "sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "dev": true, "license": "MIT", - "dependencies": { - "slice-ansi": "^9.0.0", - "string-width": "^8.2.0" - }, "engines": { - "node": ">=22" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/code-excerpt": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/code-excerpt/-/code-excerpt-4.0.0.tgz", - "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, "license": "MIT", "dependencies": { - "convert-to-spaces": "^2.0.1" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", "dev": true, "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" - }, - "node_modules/convert-to-spaces": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", - "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">= 8" + "node": "18 || 20 || >=22" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "ms": "^2.1.3" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=6.0" + "node": "18 || 20 || >=22" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.6", + "resolved": "https://registry.npmmirror.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.6.tgz", + "integrity": "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@vegamo/deepcode-cli": { + "resolved": "packages/cli", + "link": true + }, + "node_modules/@vegamo/deepcode-core": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@vegamo/deepcode-vscode": { + "resolved": "packages/vscode-ide-companion", + "link": true + }, + "node_modules/@vscode/vsce": { + "version": "3.9.2", + "resolved": "https://registry.npmmirror.com/@vscode/vsce/-/vsce-3.9.2.tgz", + "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^13.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^10.2.2", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmmirror.com/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmmirror.com/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cli-boxes": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-4.0.1.tgz", + "integrity": "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==", + "license": "MIT", + "engines": { + "node": ">=18.20 <19 || >=20.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" } }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmmirror.com/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/ejs": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-5.0.2.tgz", + "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==", + "license": "Apache-2.0", + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.12.18" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.47.1", + "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.47.1.tgz", + "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gradient-string": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/gradient-string/-/gradient-string-3.0.0.tgz", + "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "tinygradient": "^1.1.5" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gradient-string/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ink": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.6.tgz", + "integrity": "sha512-/KG651f+LHln9gumb5ltieFqzNGJdhX1b/WwsCUd2Py7Htuk9KUzyFrk25ugmzjXyDneXSoXD3cm4ql4dWFGsQ==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.3.0", + "ansi-escapes": "^7.3.0", + "ansi-styles": "^6.2.3", + "auto-bind": "^5.0.1", + "chalk": "^5.6.2", + "cli-boxes": "^4.0.1", + "cli-cursor": "^4.0.0", + "cli-truncate": "^6.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.45.1", + "indent-string": "^5.0.0", + "is-in-ci": "^2.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.33.0", + "scheduler": "^0.27.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^9.0.0", + "stack-utils": "^2.0.6", + "string-width": "^8.2.0", + "terminal-size": "^4.0.1", + "type-fest": "^5.5.0", + "widest-line": "^6.0.0", + "wrap-ansi": "^10.0.0", + "ws": "^8.20.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@types/react": ">=19.2.0", + "react": ">=19.2.0", + "react-devtools-core": ">=6.1.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-gradient": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/ink-gradient/-/ink-gradient-4.0.1.tgz", + "integrity": "sha512-0ckdiM84zkfCdnTtcnq4BS3egIhUPPDoCqSx/7NUFsAVooBbdRuGnnWpk0fuaOTqU6rlZRh9F4LN1UI8fxd81Q==", + "license": "MIT", + "dependencies": { + "@types/gradient-string": "^1.1.6", + "gradient-string": "^3.0.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "peerDependencies": { + "ink": ">=6", + "react": ">=19.2.0" + } + }, + "node_modules/ink/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ink/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/cli-truncate": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-6.0.0.tgz", + "integrity": "sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^9.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/ink/node_modules/slice-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-9.0.0.tgz", + "integrity": "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/ejs": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz", - "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==", - "license": "Apache-2.0", + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", "bin": { - "ejs": "bin/cli.js" + "is-in-ci": "cli.js" }, "engines": { - "node": ">=0.12.18" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.353", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", - "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, "engines": { - "node": ">=18" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-toolkit": { - "version": "1.46.1", - "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", - "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmmirror.com/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "node": ">=0.10.0" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.8.0" } }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "node_modules/linkify-it": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lint-staged": { + "version": "17.0.7", + "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-17.0.7.tgz", + "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "listr2": "^10.2.1", + "picomatch": "^4.0.4", + "string-argv": "^0.3.2", + "tinyexec": "^1.2.4" }, "bin": { - "eslint": "bin/eslint.js" + "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=22.22.1" }, "funding": { - "url": "https://eslint.org/donate" + "url": "https://opencollective.com/lint-staged" }, - "peerDependencies": { - "jiti": "*" + "optionalDependencies": { + "yaml": "^2.9.0" + } + }, + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmmirror.com/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "engines": { + "node": ">=22.13.0" } }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" + "dependencies": { + "p-locate": "^5.0.0" }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" + "engines": { + "node": ">=10" }, - "peerDependencies": { - "eslint": ">=7.0.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { "node": ">=18" }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.1", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">= 0.4" } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 8" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.1.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=0.10" + "node": ">=8.6" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, "engines": { - "node": ">=4.0" + "node": ">=4" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "license": "MIT", "dependencies": { - "is-extendable": "^0.1.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { + "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "license": "MIT", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=6" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, "engines": { - "node": ">=16.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, + "optional": true, "engines": { "node": ">=10" }, @@ -2180,662 +5234,517 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16" + "node": "*" } }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=6.9.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", "dev": true, "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", + "optional": true, "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "semver": "^7.3.5" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=10" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" + "optional": true, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=10.13.0" + "node": ">=10" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } + "optional": true }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.5" + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=20" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gradient-string": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/gradient-string/-/gradient-string-3.0.0.tgz", - "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "chalk": "^5.3.0", - "tinygradient": "^1.1.5" + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": ">=14" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "license": "MIT", + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" + "lru-cache": "^10.0.1" }, "engines": { - "node": ">=6.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "ISC" }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, - "license": "MIT" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "hermes-estree": "0.25.1" + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", - "bin": { - "husky": "bin.js" - }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/typicode" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "license": "MIT", - "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ink": { - "version": "7.0.4", - "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.4.tgz", - "integrity": "sha512-4wsM/gMKOT2ZANNTJibI6I9IcwBfobqv/CgaDcwvOaCREZIQxo3iGQS7qPHa2hmA67NYltZWCMtBDELB/mcbJQ==", - "license": "MIT", - "dependencies": { - "@alcalzone/ansi-tokenize": "^0.3.0", - "ansi-escapes": "^7.3.0", - "ansi-styles": "^6.2.3", - "auto-bind": "^5.0.1", - "chalk": "^5.6.2", - "cli-boxes": "^4.0.1", - "cli-cursor": "^4.0.0", - "cli-truncate": "^6.0.0", - "code-excerpt": "^4.0.0", - "es-toolkit": "^1.45.1", - "indent-string": "^5.0.0", - "is-in-ci": "^2.0.0", - "patch-console": "^2.0.0", - "react-reconciler": "^0.33.0", - "scheduler": "^0.27.0", - "signal-exit": "^3.0.7", - "slice-ansi": "^9.0.0", - "stack-utils": "^2.0.6", - "string-width": "^8.2.0", - "terminal-size": "^4.0.1", - "type-fest": "^5.5.0", - "widest-line": "^6.0.0", - "wrap-ansi": "^10.0.0", - "ws": "^8.20.0", - "yoga-layout": "~3.2.1" - }, - "engines": { - "node": ">=22" - }, + "node_modules/openai": { + "version": "6.42.0", + "resolved": "https://registry.npmmirror.com/openai/-/openai-6.42.0.tgz", + "integrity": "sha512-1WFEt/uXMXOLhYRNkgJWo08Y2YNvNwpVU72K7ibrWgWpNOXd4VojXLbe6SQ4bLiUQ3Y8jz4IiyVkylJCL1DtZg==", + "license": "Apache-2.0", "peerDependencies": { - "@types/react": ">=19.2.0", - "react": ">=19.2.0", - "react-devtools-core": ">=6.1.2" + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" }, "peerDependenciesMeta": { - "@types/react": { + "ws": { "optional": true }, - "react-devtools-core": { + "zod": { "optional": true } } }, - "node_modules/ink-gradient": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/ink-gradient/-/ink-gradient-4.0.1.tgz", - "integrity": "sha512-0ckdiM84zkfCdnTtcnq4BS3egIhUPPDoCqSx/7NUFsAVooBbdRuGnnWpk0fuaOTqU6rlZRh9F4LN1UI8fxd81Q==", - "license": "MIT", - "dependencies": { - "@types/gradient-string": "^1.1.6", - "gradient-string": "^3.0.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "ink": ">=6", - "react": ">=19.2.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.3.1" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-in-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/is-in-ci/-/is-in-ci-2.0.0.tgz", - "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, "license": "MIT", - "bin": { - "is-in-ci": "cli.js" - }, "engines": { - "node": ">=20" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "callsites": "^3.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=6" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "dev": true, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=6" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "semver": "^5.1.0" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "entities": "^6.0.0" }, - "engines": { - "node": ">= 0.8.0" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/lint-staged": { - "version": "17.0.4", - "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-17.0.4.tgz", - "integrity": "sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA==", + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "license": "MIT", "dependencies": { - "listr2": "^10.2.1", - "picomatch": "^4.0.4", - "string-argv": "^0.3.2", - "tinyexec": "^1.1.2" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=22.22.1" + "domhandler": "^5.0.3", + "parse5": "^7.0.0" }, "funding": { - "url": "https://opencollective.com/lint-staged" - }, - "optionalDependencies": { - "yaml": "^2.8.4" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/listr2": { - "version": "10.2.1", - "resolved": "https://registry.npmmirror.com/listr2/-/listr2-10.2.1.tgz", - "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^5.2.0", - "eventemitter3": "^5.0.4", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^10.0.0" + "parse5": "^7.0.0" }, - "engines": { - "node": ">=22.13.0" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/listr2/node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">=20" + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/listr2/node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/log-update/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "restore-cursor": "^5.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=18" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/log-update/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "dev": true, "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, "engines": { "node": ">=18" }, @@ -2843,517 +5752,726 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/log-update/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=4" } }, - "node_modules/log-update/node_modules/slice-ansi": { + "node_modules/prebuild-install": { "version": "7.1.2", - "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" }, - "engines": { - "node": ">=18" + "bin": { + "prebuild-install": "bin.js" }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "engines": { + "node": ">=10" } }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8.0" } }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "bin": { + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=18" + "node": ">=14" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, - "license": "ISC", + "license": "MIT", + "optional": true, "dependencies": { - "yallist": "^3.0.2" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, - "license": "ISC", + "license": "BSD-3-Clause", "dependencies": { - "brace-expansion": "^1.1.7" + "side-channel": "^1.1.0" }, "engines": { - "node": "*" + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, - "license": "MIT" + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } }, - "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/openai": { - "version": "6.35.0", - "resolved": "https://registry.npmmirror.com/openai/-/openai-6.35.0.tgz", - "integrity": "sha512-L/skwIGnt5xQZHb0UfTu9uAUKbis3ehKypOuJKi20QvG7UStV6C8IC3myGYHcdiF4kms/bAvOJ9UqqNWqi8x/Q==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" + "engines": { + "node": ">=0.10.0" }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } + "peerDependencies": { + "react": "^19.2.0" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "mute-stream": "~0.0.4" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.8" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "callsites": "^3.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">=6" + "node": ">= 6" } }, - "node_modules/patch-console": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/patch-console/-/patch-console-2.0.0.tgz", - "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=0.10.0" } }, - "node_modules/path-exists": { + "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "engines": { - "node": "20 || >=22" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 0.8.0" + "node": ">=11.0.0" } }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", "dev": true, "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, "bin": { - "prettier": "bin/prettier.cjs" + "secretlint": "bin/secretlint.js" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "node": ">=20.0.0" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, "engines": { - "node": ">=6" + "node": ">=4" } }, - "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmmirror.com/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/react-reconciler": { - "version": "0.33.0", - "resolved": "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.33.0.tgz", - "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^19.2.0" + "node": ">=8" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", + "optional": true, "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/slice-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-9.0.0.tgz", - "integrity": "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==", + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=22" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -3372,6 +6490,26 @@ "node": ">=10" } }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", @@ -3435,601 +6573,383 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", - "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-size": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/terminal-size/-/terminal-size-4.0.1.tgz", - "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinygradient": { - "version": "1.1.5", - "resolved": "https://registry.npmmirror.com/tinygradient/-/tinygradient-1.1.5.tgz", - "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", - "license": "MIT", - "dependencies": { - "@types/tinycolor2": "^1.4.0", - "tinycolor2": "^1.0.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" } }, - "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, "engines": { - "node": ">=18" + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, - "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmmirror.com/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=18" + "node": ">=10.0.0" } }, - "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], + "node_modules/table/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" } }, - "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, "engines": { - "node": ">=18" + "node": ">=6" } }, - "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/terminal-size": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/terminal-size/-/terminal-size-4.0.1.tgz", + "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmmirror.com/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, "engines": { - "node": ">=18" + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" } }, - "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { "node": ">=18" } }, - "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=18" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/tinygradient": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/tinygradient/-/tinygradient-1.1.5.tgz", + "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@types/tinycolor2": "^1.4.0", + "tinycolor2": "^1.0.0" } }, - "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=14.14" } }, - "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "is-number": "^7.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8.0" } }, - "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/tsx/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, - "hasInstallScript": true, "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, "bin": { - "esbuild": "bin/esbuild" + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">=18" + "node": ">=18.0.0" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" } }, "node_modules/type-check": { @@ -4046,9 +6966,9 @@ } }, "node_modules/type-fest": { - "version": "5.6.0", - "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-5.6.0.tgz", - "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "version": "5.7.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-5.7.0.tgz", + "integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -4060,6 +6980,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmmirror.com/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", @@ -4075,16 +7007,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "version": "8.61.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4098,22 +7030,58 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmmirror.com/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.27.2.tgz", + "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", "license": "MIT", "engines": { "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.24.6", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -4155,6 +7123,69 @@ "punycode": "^2.1.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmmirror.com/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -4213,9 +7244,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/ws": { "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { @@ -4234,6 +7285,46 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", @@ -4242,9 +7333,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "optional": true, @@ -4258,6 +7349,29 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -4298,6 +7412,117 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "packages/cli": { + "name": "@vegamo/deepcode-cli", + "version": "0.1.30", + "license": "MIT", + "dependencies": { + "@vegamo/deepcode-core": "file:../core", + "chalk": "^5.6.2", + "gradient-string": "^3.0.0", + "ignore": "^7.0.5", + "ink": "^7.0.4", + "ink-gradient": "^4.0.1", + "react": "^19.2.5" + }, + "bin": { + "deepcode": "dist/cli.js" + }, + "engines": { + "node": ">=22" + } + }, + "packages/cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "packages/cli/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "packages/core": { + "name": "@vegamo/deepcode-core", + "version": "0.1.30", + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "ejs": "^5.0.2", + "gray-matter": "^4.0.3", + "ignore": "^7.0.5", + "openai": "^6.35.0", + "undici": "^7.25.0", + "zod": "^4.4.3" + } + }, + "packages/core/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "packages/core/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "packages/vscode": { + "name": "@vegamo/deepcode-vscode", + "version": "0.1.22", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@vegamo/deepcode-core": "*", + "markdown-it": "^14.1.1", + "openai": "^6.35.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.1", + "@types/vscode": "^1.85.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "packages/vscode-ide-companion": { + "name": "@vegamo/deepcode-vscode", + "version": "0.1.22", + "license": "MIT", + "dependencies": { + "@vegamo/deepcode-core": "file:../core", + "markdown-it": "^14.2.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.1", + "@types/vscode": "^1.85.0", + "@vscode/vsce": "^3.6.0" + }, + "engines": { + "vscode": "^1.85.0" + } } } } diff --git a/package.json b/package.json index add0e271..a0e4e793 100644 --- a/package.json +++ b/package.json @@ -1,57 +1,33 @@ { - "name": "@vegamo/deepcode-cli", - "version": "0.1.30", - "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", + "name": "deepcode", + "description": "Deep Code — CLI, core library, and VSCode companion", "license": "MIT", + "packageManager": "npm@10.9.4", "type": "module", + "workspaces": [ + "packages/*" + ], "repository": { "type": "git", - "url": "https://github.com/lessweb/deepcode-cli.git" - }, - "homepage": "https://deepcode.vegamo.cn", - "bin": { - "deepcode": "./dist/cli.js" - }, - "main": "./dist/cli.js", - "files": [ - "dist/cli.js", - "dist/bundled/**", - "templates/tools/**", - "templates/prompts/**", - "templates/skills/**", - "README.md", - "LICENSE" - ], - "engines": { - "node": ">=22" + "url": "git+https://github.com/lessweb/deepcode-cli.git" }, "scripts": { - "typecheck": "tsc -p ./ --noEmit", - "bundle": "esbuild ./src/cli.tsx --bundle --platform=node --format=esm --target=node18 --outfile=dist/cli.js --banner:js=\"#!/usr/bin/env node\" --jsx=automatic --jsx-import-source=react --packages=external --log-override:empty-import-meta=silent && node scripts/copy_bundle_assets.js", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix", - "format": "prettier --write 'src/**/*.{ts,tsx}'", - "format:check": "prettier --check 'src/**/*.{ts,tsx}'", + "typecheck": "npm run typecheck --workspaces --if-present", + "generate": "node scripts/generate-git-commit-info.js", + "bundle": "npm run generate && node scripts/esbuild.config.js && node scripts/copy-bundle-assets.js", + "lint": "eslint 'packages/*/src/**/*.{ts,tsx}' 'scripts/*.js'", + "lint:fix": "eslint 'packages/*/src/**/*.{ts,tsx}' 'scripts/*.js' --fix", + "format": "prettier --write 'packages/*/src/**/*.{ts,tsx}' 'scripts/*.js'", + "format:check": "prettier --check 'packages/*/src/**/*.{ts,tsx}' 'scripts/*.js'", "check": "npm run typecheck && npm run lint && npm run format:check", - "build": "npm run check && npm run bundle && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"", - "test": "node src/tests/run-tests.mjs", - "test:single": "tsx --test", - "prepack": "npm run build", + "clean": "node scripts/clean.js", + "build": "node scripts/build.js", + "build:vscode": "node scripts/build-vscode-companion.js", + "start": "node scripts/start.js", + "build-and-start": "npm run build && npm run start", + "test": "npm run test --workspaces --if-present", "prepare": "husky" }, - "dependencies": { - "chalk": "^5.6.2", - "ejs": "^5.0.2", - "gradient-string": "^3.0.0", - "gray-matter": "^4.0.3", - "ignore": "^7.0.5", - "ink": "^7.0.4", - "ink-gradient": "^4.0.1", - "openai": "^6.35.0", - "react": "^19.2.5", - "undici": "^7.25.0", - "zod": "^4.4.3" - }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/ejs": "^3.1.5", diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..21e7ae8e --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,42 @@ +{ + "name": "@vegamo/deepcode-cli", + "version": "0.1.30", + "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", + "license": "MIT", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/lessweb/deepcode-cli.git" + }, + "homepage": "https://deepcode.vegamo.cn", + "bin": { + "deepcode": "./dist/cli.js" + }, + "main": "./dist/cli.js", + "files": [ + "dist/cli.js", + "dist/bundled/**", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=22" + }, + "scripts": { + "typecheck": "tsc -p ./ --noEmit", + "bundle": "node ../../scripts/esbuild.config.js", + "build": "npm run typecheck && npm run bundle && node ../../scripts/copy-bundle-assets.js && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"", + "prepublishOnly": "npm run build", + "format": "prettier --write .", + "test": "node src/tests/run-tests.mjs" + }, + "dependencies": { + "@vegamo/deepcode-core": "file:../core", + "chalk": "^5.6.2", + "gradient-string": "^3.0.0", + "ignore": "^7.0.5", + "ink": "^7.0.4", + "ink-gradient": "^4.0.1", + "react": "^19.2.5" + } +} diff --git a/src/cli.tsx b/packages/cli/src/cli.tsx similarity index 98% rename from src/cli.tsx rename to packages/cli/src/cli.tsx index 6da6505d..c595916b 100644 --- a/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,6 +1,6 @@ import React from "react"; import { render } from "ink"; -import { setShellIfWindows } from "./common/shell-utils"; +import { setShellIfWindows } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; diff --git a/src/common/update-check.ts b/packages/cli/src/common/update-check.ts similarity index 99% rename from src/common/update-check.ts rename to packages/cli/src/common/update-check.ts index 2d27c7a6..7a4710be 100644 --- a/src/common/update-check.ts +++ b/packages/cli/src/common/update-check.ts @@ -5,7 +5,7 @@ import * as os from "os"; import * as path from "path"; import { render, type Instance } from "ink"; import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; -import { killProcessTree } from "./process-tree"; +import { killProcessTree } from "@vegamo/deepcode-core"; export type PackageInfo = { name: string; diff --git a/packages/cli/src/generated/git-commit.ts b/packages/cli/src/generated/git-commit.ts new file mode 100644 index 00000000..e32594c8 --- /dev/null +++ b/packages/cli/src/generated/git-commit.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2026 @vegamo deepcode + */ + +// Auto-generated by scripts/generate-git-commit-info.js. Do not edit. +export const GIT_COMMIT_INFO = "cc7b0c3"; +export const CLI_VERSION = "0.1.30"; diff --git a/src/tests/ask-user-question.test.ts b/packages/cli/src/tests/ask-user-question.test.ts similarity index 98% rename from src/tests/ask-user-question.test.ts rename to packages/cli/src/tests/ask-user-question.test.ts index f7543512..7b4f387e 100644 --- a/src/tests/ask-user-question.test.ts +++ b/packages/cli/src/tests/ask-user-question.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, formatAskUserQuestionDecline } from "../ui"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "@vegamo/deepcode-core"; function message(content: unknown): SessionMessage { const now = "2026-04-29T00:00:00.000Z"; diff --git a/src/tests/clipboard.test.ts b/packages/cli/src/tests/clipboard.test.ts similarity index 100% rename from src/tests/clipboard.test.ts rename to packages/cli/src/tests/clipboard.test.ts diff --git a/src/tests/dropdown-menu.test.ts b/packages/cli/src/tests/dropdown-menu.test.ts similarity index 100% rename from src/tests/dropdown-menu.test.ts rename to packages/cli/src/tests/dropdown-menu.test.ts diff --git a/src/tests/exit-summary.test.ts b/packages/cli/src/tests/exit-summary.test.ts similarity index 97% rename from src/tests/exit-summary.test.ts rename to packages/cli/src/tests/exit-summary.test.ts index 5ea4b579..e0d481db 100644 --- a/src/tests/exit-summary.test.ts +++ b/packages/cli/src/tests/exit-summary.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildExitSummaryText } from "../ui"; -import type { ModelUsage, SessionEntry } from "../session"; +import type { ModelUsage, SessionEntry } from "@vegamo/deepcode-core"; const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, ""); diff --git a/src/tests/file-mentions.test.ts b/packages/cli/src/tests/file-mentions.test.ts similarity index 100% rename from src/tests/file-mentions.test.ts rename to packages/cli/src/tests/file-mentions.test.ts diff --git a/src/tests/loading-text.test.ts b/packages/cli/src/tests/loading-text.test.ts similarity index 100% rename from src/tests/loading-text.test.ts rename to packages/cli/src/tests/loading-text.test.ts diff --git a/src/tests/markdown.test.ts b/packages/cli/src/tests/markdown.test.ts similarity index 100% rename from src/tests/markdown.test.ts rename to packages/cli/src/tests/markdown.test.ts diff --git a/src/tests/message-view.test.ts b/packages/cli/src/tests/message-view.test.ts similarity index 99% rename from src/tests/message-view.test.ts rename to packages/cli/src/tests/message-view.test.ts index ff497707..fbd2b097 100644 --- a/src/tests/message-view.test.ts +++ b/packages/cli/src/tests/message-view.test.ts @@ -13,7 +13,7 @@ import { parseToolPayload, } from "../ui/components/MessageView/utils"; import { RawMode } from "../ui/contexts"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "@vegamo/deepcode-core"; import type { ToolSummary } from "../ui/components/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { diff --git a/src/tests/permission-prompt.test.ts b/packages/cli/src/tests/permission-prompt.test.ts similarity index 100% rename from src/tests/permission-prompt.test.ts rename to packages/cli/src/tests/permission-prompt.test.ts diff --git a/src/tests/prompt-buffer.test.ts b/packages/cli/src/tests/prompt-buffer.test.ts similarity index 100% rename from src/tests/prompt-buffer.test.ts rename to packages/cli/src/tests/prompt-buffer.test.ts diff --git a/src/tests/prompt-input-keys.test.ts b/packages/cli/src/tests/prompt-input-keys.test.ts similarity index 99% rename from src/tests/prompt-input-keys.test.ts rename to packages/cli/src/tests/prompt-input-keys.test.ts index bcad3395..0c5773cf 100644 --- a/src/tests/prompt-input-keys.test.ts +++ b/packages/cli/src/tests/prompt-input-keys.test.ts @@ -28,7 +28,7 @@ import { insertText, backspace, } from "../ui"; -import type { SessionMessage, SkillInfo } from "../session"; +import type { SessionMessage, SkillInfo } from "@vegamo/deepcode-core"; import { dispatchTerminalInput, parseTerminalInput } from "../ui/hooks"; function collectDispatchedInput(data: string) { diff --git a/src/tests/prompt-undo-redo.test.ts b/packages/cli/src/tests/prompt-undo-redo.test.ts similarity index 100% rename from src/tests/prompt-undo-redo.test.ts rename to packages/cli/src/tests/prompt-undo-redo.test.ts diff --git a/packages/cli/src/tests/run-tests.mjs b/packages/cli/src/tests/run-tests.mjs new file mode 100644 index 00000000..87748b2d --- /dev/null +++ b/packages/cli/src/tests/run-tests.mjs @@ -0,0 +1,15 @@ +// Test runner for @vegamo/deepcode-cli +import { globSync } from "glob"; +import { spawnSync } from "child_process"; +import { fileURLToPath } from "url"; +import * as path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const testFiles = globSync("*.test.ts", { cwd: __dirname }); + +const result = spawnSync(process.execPath, ["--import", "tsx", "--test", ...testFiles], { + stdio: "inherit", + cwd: __dirname, +}); + +process.exit(result.status ?? 1); diff --git a/src/tests/session-list.test.ts b/packages/cli/src/tests/session-list.test.ts similarity index 98% rename from src/tests/session-list.test.ts rename to packages/cli/src/tests/session-list.test.ts index 6fe41c70..654b4152 100644 --- a/src/tests/session-list.test.ts +++ b/packages/cli/src/tests/session-list.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { formatSessionTitle, filterSessions, formatSessionStatus } from "../ui"; -import type { SessionEntry } from "../session"; +import type { SessionEntry } from "@vegamo/deepcode-core"; test("formatSessionTitle replaces newlines with spaces", () => { assert.equal(formatSessionTitle("first line\nsecond line\r\nthird"), "first line second line third"); diff --git a/src/tests/slash-commands.test.ts b/packages/cli/src/tests/slash-commands.test.ts similarity index 98% rename from src/tests/slash-commands.test.ts rename to packages/cli/src/tests/slash-commands.test.ts index 30d77eeb..420e5a48 100644 --- a/src/tests/slash-commands.test.ts +++ b/packages/cli/src/tests/slash-commands.test.ts @@ -7,7 +7,7 @@ import { formatSlashCommandDescription, formatSlashCommandLabel, } from "../ui"; -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "@vegamo/deepcode-core"; const skills: SkillInfo[] = [ { name: "skill-writer", path: "~/.agents/skills/skill-writer/SKILL.md", description: "Write a SKILL.md" }, diff --git a/src/tests/thinking-state.test.ts b/packages/cli/src/tests/thinking-state.test.ts similarity index 96% rename from src/tests/thinking-state.test.ts rename to packages/cli/src/tests/thinking-state.test.ts index 8f2a0e30..efbee883 100644 --- a/src/tests/thinking-state.test.ts +++ b/packages/cli/src/tests/thinking-state.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findExpandedThinkingId } from "../ui"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "@vegamo/deepcode-core"; function buildMessage( id: string, diff --git a/src/tests/update-check.test.ts b/packages/cli/src/tests/update-check.test.ts similarity index 100% rename from src/tests/update-check.test.ts rename to packages/cli/src/tests/update-check.test.ts diff --git a/src/tests/welcome-screen.test.ts b/packages/cli/src/tests/welcome-screen.test.ts similarity index 100% rename from src/tests/welcome-screen.test.ts rename to packages/cli/src/tests/welcome-screen.test.ts diff --git a/src/ui/ascii-art.ts b/packages/cli/src/ui/ascii-art.ts similarity index 100% rename from src/ui/ascii-art.ts rename to packages/cli/src/ui/ascii-art.ts diff --git a/src/ui/components/DropdownMenu/index.tsx b/packages/cli/src/ui/components/DropdownMenu/index.tsx similarity index 100% rename from src/ui/components/DropdownMenu/index.tsx rename to packages/cli/src/ui/components/DropdownMenu/index.tsx diff --git a/src/ui/components/FileMentionMenu/index.tsx b/packages/cli/src/ui/components/FileMentionMenu/index.tsx similarity index 100% rename from src/ui/components/FileMentionMenu/index.tsx rename to packages/cli/src/ui/components/FileMentionMenu/index.tsx diff --git a/src/ui/components/MessageView/index.tsx b/packages/cli/src/ui/components/MessageView/index.tsx similarity index 100% rename from src/ui/components/MessageView/index.tsx rename to packages/cli/src/ui/components/MessageView/index.tsx diff --git a/src/ui/components/MessageView/markdown.ts b/packages/cli/src/ui/components/MessageView/markdown.ts similarity index 100% rename from src/ui/components/MessageView/markdown.ts rename to packages/cli/src/ui/components/MessageView/markdown.ts diff --git a/src/ui/components/MessageView/types.ts b/packages/cli/src/ui/components/MessageView/types.ts similarity index 84% rename from src/ui/components/MessageView/types.ts rename to packages/cli/src/ui/components/MessageView/types.ts index 743eb2dc..dc727469 100644 --- a/src/ui/components/MessageView/types.ts +++ b/packages/cli/src/ui/components/MessageView/types.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../../session"; +import type { SessionMessage } from "@vegamo/deepcode-core"; export type MessageViewProps = { message: SessionMessage; diff --git a/src/ui/components/MessageView/utils.ts b/packages/cli/src/ui/components/MessageView/utils.ts similarity index 99% rename from src/ui/components/MessageView/utils.ts rename to packages/cli/src/ui/components/MessageView/utils.ts index 91ae64be..4b6158d1 100644 --- a/src/ui/components/MessageView/utils.ts +++ b/packages/cli/src/ui/components/MessageView/utils.ts @@ -1,5 +1,5 @@ import type { DiffPreviewLine, ToolSummary } from "./types"; -import type { SessionMessage } from "../../../session"; +import type { SessionMessage } from "@vegamo/deepcode-core"; import { RawMode } from "../../contexts"; import chalk from "chalk"; diff --git a/src/ui/components/ModelsDropdown/index.tsx b/packages/cli/src/ui/components/ModelsDropdown/index.tsx similarity index 98% rename from src/ui/components/ModelsDropdown/index.tsx rename to packages/cli/src/ui/components/ModelsDropdown/index.tsx index 6e807569..9fe968b4 100644 --- a/src/ui/components/ModelsDropdown/index.tsx +++ b/packages/cli/src/ui/components/ModelsDropdown/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useInput } from "ink"; import DropdownMenu from "../DropdownMenu"; -import type { ModelConfigSelection, ReasoningEffort } from "../../../settings"; +import type { ModelConfigSelection, ReasoningEffort } from "@vegamo/deepcode-core"; type ModelStep = "model" | "thinking"; diff --git a/src/ui/components/RawModeExitPrompt/index.tsx b/packages/cli/src/ui/components/RawModeExitPrompt/index.tsx similarity index 100% rename from src/ui/components/RawModeExitPrompt/index.tsx rename to packages/cli/src/ui/components/RawModeExitPrompt/index.tsx diff --git a/src/ui/components/RawModelDropdown/index.tsx b/packages/cli/src/ui/components/RawModelDropdown/index.tsx similarity index 100% rename from src/ui/components/RawModelDropdown/index.tsx rename to packages/cli/src/ui/components/RawModelDropdown/index.tsx diff --git a/src/ui/components/SkillsDropdown/index.tsx b/packages/cli/src/ui/components/SkillsDropdown/index.tsx similarity index 97% rename from src/ui/components/SkillsDropdown/index.tsx rename to packages/cli/src/ui/components/SkillsDropdown/index.tsx index 4ec53397..1fe65ebb 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/packages/cli/src/ui/components/SkillsDropdown/index.tsx @@ -1,6 +1,6 @@ import DropdownMenu from "../DropdownMenu"; import React, { useEffect, useState } from "react"; -import type { SkillInfo } from "../../../session"; +import type { SkillInfo } from "@vegamo/deepcode-core"; import { useInput } from "ink"; import { isSkillSelected } from "../../views/SlashCommandMenu"; diff --git a/src/ui/components/index.ts b/packages/cli/src/ui/components/index.ts similarity index 100% rename from src/ui/components/index.ts rename to packages/cli/src/ui/components/index.ts diff --git a/src/ui/constants.ts b/packages/cli/src/ui/constants.ts similarity index 100% rename from src/ui/constants.ts rename to packages/cli/src/ui/constants.ts diff --git a/src/ui/contexts/AppContext.tsx b/packages/cli/src/ui/contexts/AppContext.tsx similarity index 100% rename from src/ui/contexts/AppContext.tsx rename to packages/cli/src/ui/contexts/AppContext.tsx diff --git a/src/ui/contexts/RawModeContext.tsx b/packages/cli/src/ui/contexts/RawModeContext.tsx similarity index 100% rename from src/ui/contexts/RawModeContext.tsx rename to packages/cli/src/ui/contexts/RawModeContext.tsx diff --git a/src/ui/contexts/index.ts b/packages/cli/src/ui/contexts/index.ts similarity index 100% rename from src/ui/contexts/index.ts rename to packages/cli/src/ui/contexts/index.ts diff --git a/src/ui/core/ask-user-question.ts b/packages/cli/src/ui/core/ask-user-question.ts similarity index 98% rename from src/ui/core/ask-user-question.ts rename to packages/cli/src/ui/core/ask-user-question.ts index 8a07e400..f49b191e 100644 --- a/src/ui/core/ask-user-question.ts +++ b/packages/cli/src/ui/core/ask-user-question.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../../session"; +import type { SessionMessage, SessionStatus } from "@vegamo/deepcode-core"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/core/clipboard.ts b/packages/cli/src/ui/core/clipboard.ts similarity index 100% rename from src/ui/core/clipboard.ts rename to packages/cli/src/ui/core/clipboard.ts diff --git a/src/ui/core/file-mentions.ts b/packages/cli/src/ui/core/file-mentions.ts similarity index 100% rename from src/ui/core/file-mentions.ts rename to packages/cli/src/ui/core/file-mentions.ts diff --git a/src/ui/core/loading-text.ts b/packages/cli/src/ui/core/loading-text.ts similarity index 96% rename from src/ui/core/loading-text.ts rename to packages/cli/src/ui/core/loading-text.ts index 2c965ea3..c757ce55 100644 --- a/src/ui/core/loading-text.ts +++ b/packages/cli/src/ui/core/loading-text.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../../session"; +import type { LlmStreamProgress, SessionEntry } from "@vegamo/deepcode-core"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/core/prompt-buffer.ts b/packages/cli/src/ui/core/prompt-buffer.ts similarity index 100% rename from src/ui/core/prompt-buffer.ts rename to packages/cli/src/ui/core/prompt-buffer.ts diff --git a/src/ui/core/prompt-undo-redo.ts b/packages/cli/src/ui/core/prompt-undo-redo.ts similarity index 100% rename from src/ui/core/prompt-undo-redo.ts rename to packages/cli/src/ui/core/prompt-undo-redo.ts diff --git a/src/ui/core/slash-commands.ts b/packages/cli/src/ui/core/slash-commands.ts similarity index 98% rename from src/ui/core/slash-commands.ts rename to packages/cli/src/ui/core/slash-commands.ts index 04840baa..ba5ae6ec 100644 --- a/src/ui/core/slash-commands.ts +++ b/packages/cli/src/ui/core/slash-commands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../../session"; +import type { SkillInfo } from "@vegamo/deepcode-core"; export type SlashCommandKind = | "skill" diff --git a/src/ui/core/thinking-state.ts b/packages/cli/src/ui/core/thinking-state.ts similarity index 94% rename from src/ui/core/thinking-state.ts rename to packages/cli/src/ui/core/thinking-state.ts index 02245091..0c9c5c7f 100644 --- a/src/ui/core/thinking-state.ts +++ b/packages/cli/src/ui/core/thinking-state.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../session"; +import type { SessionMessage } from "@vegamo/deepcode-core"; /** * Returns the message id of the assistant "thinking" message that should stay diff --git a/src/ui/exit-summary.ts b/packages/cli/src/ui/exit-summary.ts similarity index 98% rename from src/ui/exit-summary.ts rename to packages/cli/src/ui/exit-summary.ts index c55d9ce8..25e09b48 100644 --- a/src/ui/exit-summary.ts +++ b/packages/cli/src/ui/exit-summary.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import gradientString from "gradient-string"; -import type { ModelUsage, SessionEntry } from "../session"; +import type { ModelUsage, SessionEntry } from "@vegamo/deepcode-core"; type ExitSummaryInput = { session: SessionEntry | null; diff --git a/src/ui/hooks/cursor.ts b/packages/cli/src/ui/hooks/cursor.ts similarity index 100% rename from src/ui/hooks/cursor.ts rename to packages/cli/src/ui/hooks/cursor.ts diff --git a/src/ui/hooks/index.ts b/packages/cli/src/ui/hooks/index.ts similarity index 100% rename from src/ui/hooks/index.ts rename to packages/cli/src/ui/hooks/index.ts diff --git a/src/ui/hooks/useHistoryNavigation.ts b/packages/cli/src/ui/hooks/useHistoryNavigation.ts similarity index 100% rename from src/ui/hooks/useHistoryNavigation.ts rename to packages/cli/src/ui/hooks/useHistoryNavigation.ts diff --git a/src/ui/hooks/usePasteHandling.ts b/packages/cli/src/ui/hooks/usePasteHandling.ts similarity index 100% rename from src/ui/hooks/usePasteHandling.ts rename to packages/cli/src/ui/hooks/usePasteHandling.ts diff --git a/src/ui/hooks/useTerminalInput.ts b/packages/cli/src/ui/hooks/useTerminalInput.ts similarity index 100% rename from src/ui/hooks/useTerminalInput.ts rename to packages/cli/src/ui/hooks/useTerminalInput.ts diff --git a/src/ui/index.ts b/packages/cli/src/ui/index.ts similarity index 100% rename from src/ui/index.ts rename to packages/cli/src/ui/index.ts diff --git a/src/ui/utils/index.ts b/packages/cli/src/ui/utils/index.ts similarity index 94% rename from src/ui/utils/index.ts rename to packages/cli/src/ui/utils/index.ts index b9b61ec4..6feb0306 100644 --- a/src/ui/utils/index.ts +++ b/packages/cli/src/ui/utils/index.ts @@ -2,9 +2,9 @@ import chalk from "chalk"; import { renderMessageToStdout } from "../components/MessageView/utils"; import type { RawMode } from "../contexts"; import type { PromptDraft } from "../views/PromptInput"; -import type { ModelConfigSelection } from "../../settings"; -import type { SessionEntry, SessionMessage } from "../../session"; -import type { SessionManager } from "../../session"; +import type { ModelConfigSelection } from "@vegamo/deepcode-core"; +import type { SessionEntry, SessionMessage } from "@vegamo/deepcode-core"; +import type { SessionManager } from "@vegamo/deepcode-core"; /** * Render all messages directly to stdout for Raw mode display. diff --git a/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx similarity index 98% rename from src/ui/views/App.tsx rename to packages/cli/src/ui/views/App.tsx index bc12962a..fe1f81cf 100644 --- a/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; -import { createOpenAIClient } from "../../common/openai-client"; -import type { PermissionScope } from "../../settings"; -import { type ModelConfigSelection } from "../../settings"; +import { createOpenAIClient } from "@vegamo/deepcode-core"; +import type { PermissionScope } from "@vegamo/deepcode-core"; +import { type ModelConfigSelection } from "@vegamo/deepcode-core"; import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptInput"; import { MessageView, RawModeExitPrompt } from "../components"; import { SessionList } from "./SessionList"; @@ -31,7 +31,7 @@ import { isCurrentSessionEmpty, renderRawModeMessages, } from "../utils"; -import { resolveCurrentSettings, writeModelConfigSelection } from "../../settings"; +import { resolveCurrentSettings, writeModelConfigSelection } from "@vegamo/deepcode-core"; import { isCollapsedThinking } from "../core/thinking-state"; import { ANSI_CLEAR_SCREEN } from "../constants"; import type { @@ -43,8 +43,8 @@ import type { SkillInfo, UndoTarget, UserPromptContent, -} from "../../session"; -import { SessionManager } from "../../session"; +} from "@vegamo/deepcode-core"; +import { SessionManager } from "@vegamo/deepcode-core"; type View = "chat" | "session-list" | "undo" | "mcp-status"; diff --git a/src/ui/views/AppContainer.tsx b/packages/cli/src/ui/views/AppContainer.tsx similarity index 100% rename from src/ui/views/AppContainer.tsx rename to packages/cli/src/ui/views/AppContainer.tsx diff --git a/src/ui/views/AskUserQuestionPrompt.tsx b/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx similarity index 100% rename from src/ui/views/AskUserQuestionPrompt.tsx rename to packages/cli/src/ui/views/AskUserQuestionPrompt.tsx diff --git a/src/ui/views/McpStatusList.tsx b/packages/cli/src/ui/views/McpStatusList.tsx similarity index 99% rename from src/ui/views/McpStatusList.tsx rename to packages/cli/src/ui/views/McpStatusList.tsx index 40d2f3f4..5a68832b 100644 --- a/src/ui/views/McpStatusList.tsx +++ b/packages/cli/src/ui/views/McpStatusList.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { McpServerStatus } from "../../mcp/mcp-manager"; +import type { McpServerStatus } from "@vegamo/deepcode-core"; type Props = { statuses: McpServerStatus[]; diff --git a/src/ui/views/PermissionPrompt.tsx b/packages/cli/src/ui/views/PermissionPrompt.tsx similarity index 98% rename from src/ui/views/PermissionPrompt.tsx rename to packages/cli/src/ui/views/PermissionPrompt.tsx index 320dd7ab..c90f5e68 100644 --- a/src/ui/views/PermissionPrompt.tsx +++ b/packages/cli/src/ui/views/PermissionPrompt.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; import { useTerminalInput } from "../hooks"; -import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../../common/permissions"; -import type { PermissionScope } from "../../settings"; +import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "@vegamo/deepcode-core"; +import type { PermissionScope } from "@vegamo/deepcode-core"; export type PermissionPromptResult = { permissions: UserToolPermission[]; diff --git a/src/ui/views/ProcessStdoutView.tsx b/packages/cli/src/ui/views/ProcessStdoutView.tsx similarity index 98% rename from src/ui/views/ProcessStdoutView.tsx rename to packages/cli/src/ui/views/ProcessStdoutView.tsx index bd5e6363..f341eb82 100644 --- a/src/ui/views/ProcessStdoutView.tsx +++ b/packages/cli/src/ui/views/ProcessStdoutView.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; -import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/bash-timeout"; -import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session"; +import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "@vegamo/deepcode-core"; +import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "@vegamo/deepcode-core"; import { useTerminalInput } from "../hooks"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx similarity index 99% rename from src/ui/views/PromptInput.tsx rename to packages/cli/src/ui/views/PromptInput.tsx index c6b150cb..8124d7aa 100644 --- a/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -60,10 +60,10 @@ import { useTerminalFocusReporting, } from "../hooks"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; -import type { ModelConfigSelection, PermissionScope } from "../../settings"; +import type { ModelConfigSelection, PermissionScope } from "@vegamo/deepcode-core"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "../components"; -import type { SessionEntry, SkillInfo } from "../../session"; -import type { UserToolPermission } from "../../common/permissions"; +import type { SessionEntry, SkillInfo } from "@vegamo/deepcode-core"; +import type { UserToolPermission } from "@vegamo/deepcode-core"; export type PromptSubmission = { text: string; diff --git a/src/ui/views/SessionList.tsx b/packages/cli/src/ui/views/SessionList.tsx similarity index 99% rename from src/ui/views/SessionList.tsx rename to packages/cli/src/ui/views/SessionList.tsx index 49d94e7f..a41cae3a 100644 --- a/src/ui/views/SessionList.tsx +++ b/packages/cli/src/ui/views/SessionList.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { SessionEntry, SessionStatus } from "../../session"; +import type { SessionEntry, SessionStatus } from "@vegamo/deepcode-core"; import { truncate } from "../components/MessageView/utils"; type Props = { diff --git a/src/ui/views/SlashCommandMenu.tsx b/packages/cli/src/ui/views/SlashCommandMenu.tsx similarity index 98% rename from src/ui/views/SlashCommandMenu.tsx rename to packages/cli/src/ui/views/SlashCommandMenu.tsx index d93446de..c138bec8 100644 --- a/src/ui/views/SlashCommandMenu.tsx +++ b/packages/cli/src/ui/views/SlashCommandMenu.tsx @@ -3,7 +3,7 @@ import type { SlashCommandItem } from "../core/slash-commands"; import { ARGS_SEPARATOR } from "../constants"; import React from "react"; import { Box, Text } from "ink"; -import type { SkillInfo } from "../../session"; +import type { SkillInfo } from "@vegamo/deepcode-core"; type SlashCommandMenuProps = { items: SlashCommandItem[]; diff --git a/src/ui/views/ThemedGradient.tsx b/packages/cli/src/ui/views/ThemedGradient.tsx similarity index 100% rename from src/ui/views/ThemedGradient.tsx rename to packages/cli/src/ui/views/ThemedGradient.tsx diff --git a/src/ui/views/UndoSelector.tsx b/packages/cli/src/ui/views/UndoSelector.tsx similarity index 99% rename from src/ui/views/UndoSelector.tsx rename to packages/cli/src/ui/views/UndoSelector.tsx index 977bca26..50a99977 100644 --- a/src/ui/views/UndoSelector.tsx +++ b/packages/cli/src/ui/views/UndoSelector.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { UndoTarget } from "../../session"; +import type { UndoTarget } from "@vegamo/deepcode-core"; export type UndoRestoreMode = "code-and-conversation" | "conversation"; diff --git a/src/ui/views/UpdatePrompt.tsx b/packages/cli/src/ui/views/UpdatePrompt.tsx similarity index 100% rename from src/ui/views/UpdatePrompt.tsx rename to packages/cli/src/ui/views/UpdatePrompt.tsx diff --git a/src/ui/views/WelcomeScreen.tsx b/packages/cli/src/ui/views/WelcomeScreen.tsx similarity index 97% rename from src/ui/views/WelcomeScreen.tsx rename to packages/cli/src/ui/views/WelcomeScreen.tsx index bee7e9ae..fdcf9211 100644 --- a/src/ui/views/WelcomeScreen.tsx +++ b/packages/cli/src/ui/views/WelcomeScreen.tsx @@ -2,8 +2,8 @@ import React, { useMemo, useState } from "react"; import { Box, Text } from "ink"; import * as os from "node:os"; import path from "node:path"; -import type { SkillInfo } from "../../session"; -import type { ResolvedDeepcodingSettings } from "../../settings"; +import type { SkillInfo } from "@vegamo/deepcode-core"; +import type { ResolvedDeepcodingSettings } from "@vegamo/deepcode-core"; import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slash-commands"; import { ThemedGradient } from "./ThemedGradient"; import { AsciiLogo } from "../ascii-art"; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000..44d2799d --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "ignoreDeprecations": "6.0", + "lib": ["ES2022"], + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "types": ["node"], + "baseUrl": ".", + "paths": { + "@vegamo/deepcode-core": ["../core/src/index.ts"], + "@vegamo/deepcode-core/*": ["../core/src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "../core/src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..eb73e22a --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,39 @@ +{ + "name": "@vegamo/deepcode-core", + "version": "0.1.30", + "description": "Deep Code core library — LLM session management, tool execution, and shared utilities", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/lessweb/deepcode-cli.git" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist/**", + "templates/**" + ], + "scripts": { + "typecheck": "tsc -p ./ --noEmit", + "build": "tsc -p ./", + "prepublishOnly": "npm run build", + "format": "prettier --write .", + "test": "node src/tests/run-tests.mjs" + }, + "dependencies": { + "chalk": "^5.6.2", + "ejs": "^5.0.2", + "gray-matter": "^4.0.3", + "ignore": "^7.0.5", + "openai": "^6.35.0", + "undici": "^7.25.0", + "zod": "^4.4.3" + } +} diff --git a/src/common/bash-timeout.ts b/packages/core/src/common/bash-timeout.ts similarity index 100% rename from src/common/bash-timeout.ts rename to packages/core/src/common/bash-timeout.ts diff --git a/src/common/debug-logger.ts b/packages/core/src/common/debug-logger.ts similarity index 100% rename from src/common/debug-logger.ts rename to packages/core/src/common/debug-logger.ts diff --git a/src/common/error-logger.ts b/packages/core/src/common/error-logger.ts similarity index 100% rename from src/common/error-logger.ts rename to packages/core/src/common/error-logger.ts diff --git a/src/common/file-history.ts b/packages/core/src/common/file-history.ts similarity index 100% rename from src/common/file-history.ts rename to packages/core/src/common/file-history.ts diff --git a/src/common/file-utils.ts b/packages/core/src/common/file-utils.ts similarity index 100% rename from src/common/file-utils.ts rename to packages/core/src/common/file-utils.ts diff --git a/src/common/model-capabilities.ts b/packages/core/src/common/model-capabilities.ts similarity index 100% rename from src/common/model-capabilities.ts rename to packages/core/src/common/model-capabilities.ts diff --git a/src/common/notify.ts b/packages/core/src/common/notify.ts similarity index 100% rename from src/common/notify.ts rename to packages/core/src/common/notify.ts diff --git a/src/common/openai-client.ts b/packages/core/src/common/openai-client.ts similarity index 100% rename from src/common/openai-client.ts rename to packages/core/src/common/openai-client.ts diff --git a/src/common/openai-message-converter.ts b/packages/core/src/common/openai-message-converter.ts similarity index 100% rename from src/common/openai-message-converter.ts rename to packages/core/src/common/openai-message-converter.ts diff --git a/src/common/openai-thinking.ts b/packages/core/src/common/openai-thinking.ts similarity index 100% rename from src/common/openai-thinking.ts rename to packages/core/src/common/openai-thinking.ts diff --git a/src/common/permissions.ts b/packages/core/src/common/permissions.ts similarity index 100% rename from src/common/permissions.ts rename to packages/core/src/common/permissions.ts diff --git a/src/common/process-tree.ts b/packages/core/src/common/process-tree.ts similarity index 100% rename from src/common/process-tree.ts rename to packages/core/src/common/process-tree.ts diff --git a/src/common/shell-utils.ts b/packages/core/src/common/shell-utils.ts similarity index 100% rename from src/common/shell-utils.ts rename to packages/core/src/common/shell-utils.ts diff --git a/src/common/state.ts b/packages/core/src/common/state.ts similarity index 100% rename from src/common/state.ts rename to packages/core/src/common/state.ts diff --git a/src/common/telemetry.ts b/packages/core/src/common/telemetry.ts similarity index 100% rename from src/common/telemetry.ts rename to packages/core/src/common/telemetry.ts diff --git a/packages/core/src/common/tool-types.ts b/packages/core/src/common/tool-types.ts new file mode 100644 index 00000000..1d664a76 --- /dev/null +++ b/packages/core/src/common/tool-types.ts @@ -0,0 +1,107 @@ +import type OpenAI from "openai"; +import type { ReasoningEffort } from "../settings"; + +export type CreateOpenAIClient = () => { + client: OpenAI | null; + model: string; + baseURL?: string; + temperature?: number; + thinkingEnabled: boolean; + reasoningEffort?: ReasoningEffort; + debugLogEnabled?: boolean; + telemetryEnabled?: boolean; + notify?: string; + webSearchTool?: string; + env?: Record; + machineId?: string; +}; + +export type ToolCall = { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +}; + +export type ToolExecutionContext = { + sessionId: string; + projectRoot: string; + toolCall: ToolCall; + createOpenAIClient?: CreateOpenAIClient; + onProcessStart?: (processId: string | number, command: string) => void; + onProcessExit?: (processId: string | number) => void; + onProcessStdout?: (processId: string | number, chunk: string) => void; + onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; + onBackgroundProcessComplete?: (completion: BackgroundProcessCompletion) => void; + onBeforeFileMutation?: (filePath: string) => void; + onAfterFileMutation?: (filePath: string) => void; + bashTimeoutMs?: number; + bashMinTimeoutMs?: number; +}; + +export type ToolExecutionHooks = { + onProcessStart?: (processId: string | number, command: string) => void; + onProcessExit?: (processId: string | number) => void; + onProcessStdout?: (processId: string | number, chunk: string) => void; + onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; + onBackgroundProcessComplete?: (completion: BackgroundProcessCompletion) => void; + onBeforeFileMutation?: (filePath: string) => void; + onAfterFileMutation?: (filePath: string) => void; + shouldStop?: () => boolean; +}; + +export type BackgroundProcessCompletion = { + taskId: string; + processId: number; + command: string; + outputPath: string; + ok: boolean; + exitCode: number | null; + signal: string | null; + error?: string; + cwd: string | null; + shellPath: string; + startedAtMs: number; + completedAtMs: number; +}; + +export type ProcessTimeoutInfo = { + timeoutMs: number; + startedAtMs: number; + deadlineAtMs: number; + timedOut: boolean; +}; + +export type ProcessTimeoutControl = { + getInfo: () => ProcessTimeoutInfo; + setTimeoutMs: (timeoutMs: number) => ProcessTimeoutInfo; +}; + +export type ToolExecutionResult = { + ok: boolean; + name: string; + output?: string; + error?: string; + metadata?: Record; + awaitUserResponse?: boolean; + followUpMessages?: ToolExecutionFollowUpMessage[]; +}; + +export type ToolExecutionFollowUpMessage = { + role: "system"; + content: string; + contentParams?: unknown | null; +}; + +export type ToolHandler = ( + args: Record, + context: ToolExecutionContext +) => Promise; + +export type ToolCallExecution = { + toolCallId: string; + content: string; + result: ToolExecutionResult; +}; diff --git a/src/common/validate.ts b/packages/core/src/common/validate.ts similarity index 99% rename from src/common/validate.ts rename to packages/core/src/common/validate.ts index b1195d8d..7e274253 100644 --- a/src/common/validate.ts +++ b/packages/core/src/common/validate.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { ToolExecutionContext, ToolExecutionResult } from "../tools/executor"; +import type { ToolExecutionContext, ToolExecutionResult } from "./tool-types"; export type ValidationResult = { ok: true; input: Record } | { ok: false; error: string }; diff --git a/packages/core/src/generated/git-commit.ts b/packages/core/src/generated/git-commit.ts new file mode 100644 index 00000000..e32594c8 --- /dev/null +++ b/packages/core/src/generated/git-commit.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2026 @vegamo deepcode + */ + +// Auto-generated by scripts/generate-git-commit-info.js. Do not edit. +export const GIT_COMMIT_INFO = "cc7b0c3"; +export const CLI_VERSION = "0.1.30"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..8ad813a7 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,132 @@ +// Core library public API — used by both CLI and VSCode companion. + +// Settings +export { + resolveCurrentSettings, + resolveSettings, + resolveSettingsSources, + readSettings, + readProjectSettings, + writeSettings, + writeProjectSettings, + writeModelConfigSelection, + applyModelConfigSelection, + modelConfigKey, + getUserSettingsPath, + getProjectSettingsPath, + DEFAULT_MODEL, + DEFAULT_BASE_URL, +} from "./settings"; +export type { + DeepcodingSettings, + ResolvedDeepcodingSettings, + ModelConfigSelection, + PermissionScope, + PermissionSettings, + PermissionDefaultMode, + McpServerConfig, + ReasoningEffort, +} from "./settings"; + +// Session +export { SessionManager, getProjectCode, getCompactPromptTokenThreshold } from "./session"; +export type { + SessionMessage, + SessionEntry, + SessionStatus, + SessionsIndex, + SessionMessageRole, + MessageMeta, + UndoTarget, + UserPromptContent, + SkillInfo, + ModelUsage, + SessionProcessEntry, + BashTimeoutAdjustment, + LlmStreamProgress, +} from "./session"; + +// Prompt utilities +export { + getSystemPrompt, + getCompactPrompt, + getRuntimeContext, + getDefaultSkillPrompt, + getExtensionRoot, + getTools, + buildSkillDocumentsPrompt, +} from "./prompt"; +export type { ToolDefinition, SkillPromptDocument } from "./prompt"; + +// Tools +export { ToolExecutor } from "./tools/executor"; +export type { + CreateOpenAIClient, + ToolCall, + ToolExecutionContext, + ToolExecutionHooks, + ToolExecutionResult, + ToolHandler, + ToolCallExecution, + ProcessTimeoutInfo, + ProcessTimeoutControl, + BackgroundProcessCompletion, + ToolExecutionFollowUpMessage, +} from "./common/tool-types"; + +// Tool handlers +export { handleBashTool, clearSessionWorkingDir } from "./tools/bash-handler"; +export { handleReadTool } from "./tools/read-handler"; +export { handleWriteTool } from "./tools/write-handler"; +export { handleEditTool } from "./tools/edit-handler"; +export { handleUpdatePlanTool } from "./tools/update-plan-handler"; +export { handleWebSearchTool } from "./tools/web-search-handler"; +export { handleAskUserQuestionTool } from "./tools/ask-user-question-handler"; + +// MCP +export { McpManager } from "./mcp/mcp-manager"; +export { McpClient } from "./mcp/mcp-client"; +export type { McpServerStatus } from "./mcp/mcp-manager"; + +// Common utilities +export { createOpenAIClient } from "./common/openai-client"; +export { buildThinkingRequestOptions } from "./common/openai-thinking"; +export { readTextFileWithMetadata, writeTextFile, buildDiffPreview, ensureParentDirectory } from "./common/file-utils"; +export { normalizeFilePath, getSnippet, clearSessionState, recordFileState, getFileState } from "./common/state"; +export { GitFileHistory } from "./common/file-history"; +export { killProcessTree } from "./common/process-tree"; +export { launchNotifyScript } from "./common/notify"; +export { reportNewPrompt } from "./common/telemetry"; +export { DEEPSEEK_V4_MODELS, supportsMultimodal, defaultsToThinkingMode } from "./common/model-capabilities"; +export { findGitBashPath, resolveShellPath, setShellIfWindows } from "./common/shell-utils"; +export { logApiError } from "./common/error-logger"; +export { logOpenAIChatCompletionDebug } from "./common/debug-logger"; +export { + clampBashTimeoutMs, + DEFAULT_BASH_TIMEOUT_MS, + BASH_TIMEOUT_INCREMENT_MS, + BASH_TIMEOUT_DECREMENT_MS, +} from "./common/bash-timeout"; +export { executeValidatedTool, semanticBoolean } from "./common/validate"; +export { OpenAIMessageConverter } from "./common/openai-message-converter"; +export { + computeToolCallPermissions, + buildPermissionToolExecution, + hasUserPermissionReplies, + appendProjectPermissionAllows, + normalizeAskPermissions, + parseToolCallForPermissions, +} from "./common/permissions"; +export type { + AskPermissionRequest, + AskPermissionScope, + BashPermissionScope, + MessageToolPermission, + PermissionDecision, + PermissionToolCall, + UserToolPermission, +} from "./common/permissions"; + +// State types +export type { FileState, FileSnippet, FileLineEnding } from "./common/state"; +export type { FileReadMetadata } from "./common/file-utils"; diff --git a/src/mcp/mcp-client.ts b/packages/core/src/mcp/mcp-client.ts similarity index 100% rename from src/mcp/mcp-client.ts rename to packages/core/src/mcp/mcp-client.ts diff --git a/src/mcp/mcp-manager.ts b/packages/core/src/mcp/mcp-manager.ts similarity index 100% rename from src/mcp/mcp-manager.ts rename to packages/core/src/mcp/mcp-manager.ts diff --git a/src/prompt.ts b/packages/core/src/prompt.ts similarity index 100% rename from src/prompt.ts rename to packages/core/src/prompt.ts diff --git a/src/session.ts b/packages/core/src/session.ts similarity index 100% rename from src/session.ts rename to packages/core/src/session.ts diff --git a/src/settings.ts b/packages/core/src/settings.ts similarity index 100% rename from src/settings.ts rename to packages/core/src/settings.ts diff --git a/src/tests/debug-logger.test.ts b/packages/core/src/tests/debug-logger.test.ts similarity index 100% rename from src/tests/debug-logger.test.ts rename to packages/core/src/tests/debug-logger.test.ts diff --git a/src/tests/mcp-client.test.ts b/packages/core/src/tests/mcp-client.test.ts similarity index 100% rename from src/tests/mcp-client.test.ts rename to packages/core/src/tests/mcp-client.test.ts diff --git a/src/tests/memory-leak.test.ts b/packages/core/src/tests/memory-leak.test.ts similarity index 100% rename from src/tests/memory-leak.test.ts rename to packages/core/src/tests/memory-leak.test.ts diff --git a/src/tests/openai-message-converter.test.ts b/packages/core/src/tests/openai-message-converter.test.ts similarity index 100% rename from src/tests/openai-message-converter.test.ts rename to packages/core/src/tests/openai-message-converter.test.ts diff --git a/src/tests/openai-thinking.test.ts b/packages/core/src/tests/openai-thinking.test.ts similarity index 100% rename from src/tests/openai-thinking.test.ts rename to packages/core/src/tests/openai-thinking.test.ts diff --git a/src/tests/permissions.test.ts b/packages/core/src/tests/permissions.test.ts similarity index 100% rename from src/tests/permissions.test.ts rename to packages/core/src/tests/permissions.test.ts diff --git a/src/tests/process-tree.test.ts b/packages/core/src/tests/process-tree.test.ts similarity index 100% rename from src/tests/process-tree.test.ts rename to packages/core/src/tests/process-tree.test.ts diff --git a/src/tests/prompt.test.ts b/packages/core/src/tests/prompt.test.ts similarity index 100% rename from src/tests/prompt.test.ts rename to packages/core/src/tests/prompt.test.ts diff --git a/packages/core/src/tests/run-tests.mjs b/packages/core/src/tests/run-tests.mjs new file mode 100644 index 00000000..ce87cbd2 --- /dev/null +++ b/packages/core/src/tests/run-tests.mjs @@ -0,0 +1,15 @@ +// Test runner for @vegamo/deepcode-core +import { globSync } from "glob"; +import { spawnSync } from "child_process"; +import { fileURLToPath } from "url"; +import * as path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const testFiles = globSync("*.test.ts", { cwd: __dirname }); + +const result = spawnSync(process.execPath, ["--import", "tsx", "--test", ...testFiles], { + stdio: "inherit", + cwd: __dirname, +}); + +process.exit(result.status ?? 1); diff --git a/src/tests/session.test.ts b/packages/core/src/tests/session.test.ts similarity index 100% rename from src/tests/session.test.ts rename to packages/core/src/tests/session.test.ts diff --git a/src/tests/settings-and-notify.test.ts b/packages/core/src/tests/settings-and-notify.test.ts similarity index 100% rename from src/tests/settings-and-notify.test.ts rename to packages/core/src/tests/settings-and-notify.test.ts diff --git a/src/tests/shell-utils.test.ts b/packages/core/src/tests/shell-utils.test.ts similarity index 100% rename from src/tests/shell-utils.test.ts rename to packages/core/src/tests/shell-utils.test.ts diff --git a/src/tests/telemetry.test.ts b/packages/core/src/tests/telemetry.test.ts similarity index 100% rename from src/tests/telemetry.test.ts rename to packages/core/src/tests/telemetry.test.ts diff --git a/src/tests/tool-executor.test.ts b/packages/core/src/tests/tool-executor.test.ts similarity index 100% rename from src/tests/tool-executor.test.ts rename to packages/core/src/tests/tool-executor.test.ts diff --git a/src/tests/tool-handlers.test.ts b/packages/core/src/tests/tool-handlers.test.ts similarity index 100% rename from src/tests/tool-handlers.test.ts rename to packages/core/src/tests/tool-handlers.test.ts diff --git a/src/tests/web-search-handler.test.ts b/packages/core/src/tests/web-search-handler.test.ts similarity index 100% rename from src/tests/web-search-handler.test.ts rename to packages/core/src/tests/web-search-handler.test.ts diff --git a/src/tools/ask-user-question-handler.ts b/packages/core/src/tools/ask-user-question-handler.ts similarity index 100% rename from src/tools/ask-user-question-handler.ts rename to packages/core/src/tools/ask-user-question-handler.ts diff --git a/src/tools/bash-handler.ts b/packages/core/src/tools/bash-handler.ts similarity index 100% rename from src/tools/bash-handler.ts rename to packages/core/src/tools/bash-handler.ts diff --git a/src/tools/edit-handler.ts b/packages/core/src/tools/edit-handler.ts similarity index 100% rename from src/tools/edit-handler.ts rename to packages/core/src/tools/edit-handler.ts diff --git a/src/tools/executor.ts b/packages/core/src/tools/executor.ts similarity index 67% rename from src/tools/executor.ts rename to packages/core/src/tools/executor.ts index 53846f48..6af57c4c 100644 --- a/src/tools/executor.ts +++ b/packages/core/src/tools/executor.ts @@ -1,5 +1,3 @@ -import type OpenAI from "openai"; -import type { ReasoningEffort } from "../settings"; import { handleAskUserQuestionTool } from "./ask-user-question-handler"; import { handleBashTool } from "./bash-handler"; import { handleEditTool } from "./edit-handler"; @@ -8,105 +6,28 @@ import { handleUpdatePlanTool } from "./update-plan-handler"; import { handleWebSearchTool } from "./web-search-handler"; import { handleWriteTool } from "./write-handler"; import type { McpManager } from "../mcp/mcp-manager"; - -export type CreateOpenAIClient = () => { - client: OpenAI | null; - model: string; - baseURL?: string; - temperature?: number; - thinkingEnabled: boolean; - reasoningEffort?: ReasoningEffort; - debugLogEnabled?: boolean; - telemetryEnabled?: boolean; - notify?: string; - webSearchTool?: string; - env?: Record; - machineId?: string; -}; - -export type ToolCall = { - id: string; - type: "function"; - function: { - name: string; - arguments: string; - }; -}; - -export type ToolExecutionContext = { - sessionId: string; - projectRoot: string; - toolCall: ToolCall; - createOpenAIClient?: CreateOpenAIClient; - onProcessStart?: (processId: string | number, command: string) => void; - onProcessExit?: (processId: string | number) => void; - onProcessStdout?: (processId: string | number, chunk: string) => void; - onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; - onBackgroundProcessComplete?: (completion: BackgroundProcessCompletion) => void; - onBeforeFileMutation?: (filePath: string) => void; - onAfterFileMutation?: (filePath: string) => void; - bashTimeoutMs?: number; - bashMinTimeoutMs?: number; -}; - -export type ToolExecutionHooks = { - onProcessStart?: (processId: string | number, command: string) => void; - onProcessExit?: (processId: string | number) => void; - onProcessStdout?: (processId: string | number, chunk: string) => void; - onProcessTimeoutControl?: (processId: string | number, control: ProcessTimeoutControl | null) => void; - onBackgroundProcessComplete?: (completion: BackgroundProcessCompletion) => void; - onBeforeFileMutation?: (filePath: string) => void; - onAfterFileMutation?: (filePath: string) => void; - shouldStop?: () => boolean; -}; - -export type BackgroundProcessCompletion = { - taskId: string; - processId: number; - command: string; - outputPath: string; - ok: boolean; - exitCode: number | null; - signal: string | null; - error?: string; - cwd: string | null; - shellPath: string; - startedAtMs: number; - completedAtMs: number; -}; - -export type ProcessTimeoutInfo = { - timeoutMs: number; - startedAtMs: number; - deadlineAtMs: number; - timedOut: boolean; -}; - -export type ProcessTimeoutControl = { - getInfo: () => ProcessTimeoutInfo; - setTimeoutMs: (timeoutMs: number) => ProcessTimeoutInfo; -}; - -export type ToolExecutionResult = { - ok: boolean; - name: string; - output?: string; - error?: string; - metadata?: Record; - awaitUserResponse?: boolean; - followUpMessages?: ToolExecutionFollowUpMessage[]; -}; - -export type ToolExecutionFollowUpMessage = { - role: "system"; - content: string; - contentParams?: unknown | null; -}; - -export type ToolHandler = ( - args: Record, - context: ToolExecutionContext -) => Promise; +import type { + CreateOpenAIClient, + ToolCall, + ToolExecutionHooks, + ToolExecutionResult, + ToolHandler, + ToolCallExecution, +} from "../common/tool-types"; + +export type { + CreateOpenAIClient, + ToolCall, + ToolExecutionContext, + ToolExecutionHooks, + ToolExecutionResult, + ToolHandler, + ToolCallExecution, + ProcessTimeoutInfo, + ProcessTimeoutControl, + BackgroundProcessCompletion, + ToolExecutionFollowUpMessage, +} from "../common/tool-types"; const BUILT_IN_TOOL_NAME_ALIASES = new Map([ ["Bash", "bash"], @@ -115,12 +36,6 @@ const BUILT_IN_TOOL_NAME_ALIASES = new Map([ ["Edit", "edit"], ]); -export type ToolCallExecution = { - toolCallId: string; - content: string; - result: ToolExecutionResult; -}; - export class ToolExecutor { private readonly projectRoot: string; private readonly createOpenAIClient?: CreateOpenAIClient; @@ -216,7 +131,6 @@ export class ToolExecutor { const handlerName = BUILT_IN_TOOL_NAME_ALIASES.get(toolName) ?? toolName; const handler = this.toolHandlers.get(handlerName); if (!handler) { - // Try MCP tools if (this.mcpManager?.isMcpTool(toolName)) { const parsedArgs = this.parseToolArguments(toolCall.function.arguments); const args = parsedArgs.ok ? parsedArgs.args : {}; diff --git a/src/tools/read-handler.ts b/packages/core/src/tools/read-handler.ts similarity index 100% rename from src/tools/read-handler.ts rename to packages/core/src/tools/read-handler.ts diff --git a/src/tools/update-plan-handler.ts b/packages/core/src/tools/update-plan-handler.ts similarity index 100% rename from src/tools/update-plan-handler.ts rename to packages/core/src/tools/update-plan-handler.ts diff --git a/src/tools/web-search-handler.ts b/packages/core/src/tools/web-search-handler.ts similarity index 100% rename from src/tools/web-search-handler.ts rename to packages/core/src/tools/web-search-handler.ts diff --git a/src/tools/write-handler.ts b/packages/core/src/tools/write-handler.ts similarity index 100% rename from src/tools/write-handler.ts rename to packages/core/src/tools/write-handler.ts diff --git a/templates/prompts/init_command.md.ejs b/packages/core/templates/prompts/init_command.md.ejs similarity index 100% rename from templates/prompts/init_command.md.ejs rename to packages/core/templates/prompts/init_command.md.ejs diff --git a/templates/skills/bundled/deepcode-self-refer/SKILL.md b/packages/core/templates/skills/bundled/deepcode-self-refer/SKILL.md similarity index 81% rename from templates/skills/bundled/deepcode-self-refer/SKILL.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/SKILL.md index 357868cd..5a8b377c 100644 --- a/templates/skills/bundled/deepcode-self-refer/SKILL.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/SKILL.md @@ -31,14 +31,14 @@ Use this Skill when the user asks any question about Deep Code itself, such as: Map the user's question to the appropriate document(s): -| Topic | Document | Key contents | -|-------|----------|-------------| -| **Overview, features, quick start** | `references/README.md` | Installation, slash commands, keyboard shortcuts, supported models, FAQ | -| **Configuration & settings** | `references/configuration.md` | `settings.json` fields, config hierarchy, env vars, thinking mode, reasoning effort, webSearchTool, enabledSkills | -| **MCP setup & usage** | `references/mcp.md` | MCP server config format, GitHub/Playwright/Filesystem examples, tool naming (`mcp____`), troubleshooting | -| **Permissions** | `references/permission.md` | Permission scopes (10 types), allow/deny/ask/defaultMode config, priority rules, persistence | -| **Notifications** | `references/notify.md` | Notify script path, injected env vars, Slack/Feishu/iTerm2/macOS/Linux/Windows examples | -| **Session persistence** | `references/session-persistence.md` | Storage paths, JSONL format, session index, compaction, `/undo` mechanics, code snapshots | +| Topic | Document | Key contents | +| ----------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| **Overview, features, quick start** | `references/README.md` | Installation, slash commands, keyboard shortcuts, supported models, FAQ | +| **Configuration & settings** | `references/configuration.md` | `settings.json` fields, config hierarchy, env vars, thinking mode, reasoning effort, webSearchTool, enabledSkills | +| **MCP setup & usage** | `references/mcp.md` | MCP server config format, GitHub/Playwright/Filesystem examples, tool naming (`mcp____`), troubleshooting | +| **Permissions** | `references/permission.md` | Permission scopes (10 types), allow/deny/ask/defaultMode config, priority rules, persistence | +| **Notifications** | `references/notify.md` | Notify script path, injected env vars, Slack/Feishu/iTerm2/macOS/Linux/Windows examples | +| **Session persistence** | `references/session-persistence.md` | Storage paths, JSONL format, session index, compaction, `/undo` mechanics, code snapshots | ### Step 2: Read the relevant document(s) @@ -58,6 +58,7 @@ Use the `Read` tool to read the appropriate document(s) from the list above. All ### Step 4: Handle common request patterns **"列出/查看可用的 skills":** + - Treat `/skills` as the canonical UI for listing currently available skills. - If answering directly, do not infer the list only from loaded skill prompts or from project/user directories. Enumerate all discovery roots: 1. `./.deepcode/skills//SKILL.md` @@ -77,18 +78,21 @@ Use the `Read` tool to read the appropriate document(s) from the list above. All - Mention that `/skills` can be used to verify the result and `enabledSkills` can enable/disable specific skills by name. **"配置 MCP":** + - Read `references/mcp.md` for the MCP format and examples - Ask the user for any required credentials (e.g., GitHub token) - Provide the exact `mcpServers` JSON block to add to `settings.json` - Mention using `/mcp` to verify the setup afterwards **"如何配置/修改 <设置项>":** + - Read `references/configuration.md` - Explain which `settings.json` field controls the setting - Clarify user-level (`~/.deepcode/settings.json`) vs project-level (`.deepcode/settings.json`) - Provide the exact JSON snippet **"<斜杠命令> 是做什么的?":** + - Read the slash command table from references/README.md - Provide a brief explanation with any additional context from relevant docs diff --git a/templates/skills/bundled/deepcode-self-refer/references/README.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/README.md similarity index 87% rename from templates/skills/bundled/deepcode-self-refer/references/README.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/README.md index de34da9a..9a4e27e0 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/README.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/README.md @@ -51,46 +51,48 @@ npm install -g @vegamo/deepcode-cli ## 主要功能 ### **Skills** + Deep Code CLI 支持 agent skills,允许您扩展助手的能力: Skills 会按以下优先级扫描: -| Scope | Path | Purpose | -| :------ | :-------------------- | :---------------------------- | -| Project | `./.deepcode/skills/` | Deep Code 原生位置,最高优先级 | -| Project | `./.agents/skills/` | 跨客户端互操作 | -| User | `~/.deepcode/skills/` | Deep Code 原生位置 | -| User | `~/.agents/skills/` | 跨客户端互操作 | +| Scope | Path | Purpose | +| :------ | :------------------------- | :-------------------------------- | +| Project | `./.deepcode/skills/` | Deep Code 原生位置,最高优先级 | +| Project | `./.agents/skills/` | 跨客户端互操作 | +| User | `~/.deepcode/skills/` | Deep Code 原生位置 | +| User | `~/.agents/skills/` | 跨客户端互操作 | | Bundled | `bundled:/SKILL.md` | Deep Code 内置 skills,最低优先级 | ### **为 DeepSeek 优化** + - 专门为 DeepSeek 模型性能调优。 - 通过使用[上下文缓存](https://api-docs.deepseek.com/guides/kv_cache)来降低成本。 - 原生支持[思考模式](https://api-docs.deepseek.com/guides/thinking_mode)和思考强度控制。 ## 斜杠命令与按键功能 -| 斜杠命令 | 操作 | -|-------------|----------------------------------| -| `/` | 打开 skills / 命令菜单 | -| `/new` | 开始新对话 | -| `/resume` | 选择历史对话继续 | -| `/continue` | 继续当前对话,或选择历史对话恢复 | -| `/model` | 切换模型、思考模式和推理强度 | +| 斜杠命令 | 操作 | +| ----------- | -------------------------------------------- | +| `/` | 打开 skills / 命令菜单 | +| `/new` | 开始新对话 | +| `/resume` | 选择历史对话继续 | +| `/continue` | 继续当前对话,或选择历史对话恢复 | +| `/model` | 切换模型、思考模式和推理强度 | | `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯) | -| `/init` | 初始化 AGENTS.md 文件 | -| `/skills` | 列出可用 skills | +| `/init` | 初始化 AGENTS.md 文件 | +| `/skills` | 列出可用 skills | | `/mcp` | 查看 MCP 服务器状态和可用工具 | -| `/undo` | 将代码和/或对话恢复到之前的状态 | -| `/exit` | 退出(也可用连续 `Ctrl+D`) | +| `/undo` | 将代码和/或对话恢复到之前的状态 | +| `/exit` | 退出(也可用连续 `Ctrl+D`) | -| 按键 | 操作 | -|---------------|--------------------| -| `Enter` | 发送消息 | +| 按键 | 操作 | +| ------------- | --------------------------- | +| `Enter` | 发送消息 | | `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | -| `Ctrl+V` | 从剪贴板粘贴图片 | -| `Esc` | 中断当前模型回复 | -| 连续 `Ctrl+D` | 退出 | +| `Ctrl+V` | 从剪贴板粘贴图片 | +| `Esc` | 中断当前模型回复 | +| 连续 `Ctrl+D` | 退出 | ## 支持的模型 @@ -98,7 +100,6 @@ Skills 会按以下优先级扫描: - `deepseek-v4-flash` - 任何其他 OpenAI 兼容模型 - ## 常见问题 ### Deep Code 是否有 VSCode 插件? diff --git a/templates/skills/bundled/deepcode-self-refer/references/configuration.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/configuration.md similarity index 64% rename from templates/skills/bundled/deepcode-self-refer/references/configuration.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/configuration.md index 2f198b10..ad437ab8 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/configuration.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/configuration.md @@ -4,53 +4,53 @@ 配置按以下优先级顺序应用(数字较小的会被数字较大的覆盖): -| 层级 | 配置来源 | 说明 | -| ---- | ------------ | ------------------------------------------- | -| 1 | 默认值 | 应用程序内硬编码的默认值 | -| 2 | 用户设置文件 | 当前用户的全局设置 | -| 3 | 项目设置文件 | 项目特定的设置 | -| 4 | 环境变量 | 系统范围或会话特定的变量 | +| 层级 | 配置来源 | 说明 | +| ---- | ------------ | ------------------------ | +| 1 | 默认值 | 应用程序内硬编码的默认值 | +| 2 | 用户设置文件 | 当前用户的全局设置 | +| 3 | 项目设置文件 | 项目特定的设置 | +| 4 | 环境变量 | 系统范围或会话特定的变量 | ## 设置文件 Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两个层级的存放位置: -| 文件类型 | 位置 | 作用范围 | -| ------------ | ---------------------------------- | ---------------------------------------------------- | -| 用户设置文件 | `~/.deepcode/settings.json` | 适用于当前用户的所有 Deep Code 会话。 | +| 文件类型 | 位置 | 作用范围 | +| ------------ | ------------------------------------ | --------------------------------------------------------------- | +| 用户设置文件 | `~/.deepcode/settings.json` | 适用于当前用户的所有 Deep Code 会话。 | | 项目设置文件 | `项目根目录/.deepcode/settings.json` | 仅在该特定项目中运行 Deep Code 时生效。项目设置会覆盖用户设置。 | ### `settings.json` 中的可用设置 以下是 `settings.json` 支持的全部顶层字段,以及 `env` 内部支持的子字段: -| 字段 | 类型 | 说明 | -| -------------------- | --------- | ------------------------------------------------------------------- | -| `env` | object | 环境变量分组(见下方子字段表) | -| `model` | string | 模型名称。优先级高于 `env.MODEL` | -| `thinkingEnabled` | boolean | 是否启用思考模式(DeepSeek V4 系列默认启用) | -| `reasoningEffort` | string | 推理强度,可选 `"high"` 或 `"max"`(默认 `"max"`) | -| `debugLogEnabled` | boolean | 是否启用调试日志输出(默认 `false`) | -| `telemetryEnabled` | boolean | 是否启用匿名使用数据上报(默认 `true`) | -| `notify` | string | 任务完成通知脚本的完整路径(如 Slack 通知脚本) | -| `webSearchTool` | string | 自定义联网搜索脚本的完整路径 | -| `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) | -| `temperature` | number | 模型采样温度,范围 `0` 到 `2` | -| `enabledSkills` | object | 按 skill 名称启用或禁用 skill 的配置 | +| 字段 | 类型 | 说明 | +| ------------------ | ------- | ------------------------------------------------------- | +| `env` | object | 环境变量分组(见下方子字段表) | +| `model` | string | 模型名称。优先级高于 `env.MODEL` | +| `thinkingEnabled` | boolean | 是否启用思考模式(DeepSeek V4 系列默认启用) | +| `reasoningEffort` | string | 推理强度,可选 `"high"` 或 `"max"`(默认 `"max"`) | +| `debugLogEnabled` | boolean | 是否启用调试日志输出(默认 `false`) | +| `telemetryEnabled` | boolean | 是否启用匿名使用数据上报(默认 `true`) | +| `notify` | string | 任务完成通知脚本的完整路径(如 Slack 通知脚本) | +| `webSearchTool` | string | 自定义联网搜索脚本的完整路径 | +| `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) | +| `temperature` | number | 模型采样温度,范围 `0` 到 `2` | +| `enabledSkills` | object | 按 skill 名称启用或禁用 skill 的配置 | #### `env` 子字段 -| 字段 | 类型 | 说明 | -| ---------- | ------ | ------------------------------------------------------------------ | -| `MODEL` | string | 模型名称。例如 `"deepseek-v4-pro"`、`"deepseek-v4-flash"` | -| `BASE_URL` | string | API 请求的基础 URL。例如 `"https://api.deepseek.com"` | -| `API_KEY` | string | API 密钥 | -| `TEMPERATURE` | string | Chat Completions 采样温度,范围 `"0"` 到 `"2"` | -| `THINKING_ENABLED` | string | 是否启用思考模式 | -| `REASONING_EFFORT` | string | 推理强度 | -| `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 | -| `TELEMETRY_ENABLED` | string | 是否启用匿名使用数据上报 | -| `<其他任意KEY>` | string | 自定义环境变量 | +| 字段 | 类型 | 说明 | +| ------------------- | ------ | --------------------------------------------------------- | +| `MODEL` | string | 模型名称。例如 `"deepseek-v4-pro"`、`"deepseek-v4-flash"` | +| `BASE_URL` | string | API 请求的基础 URL。例如 `"https://api.deepseek.com"` | +| `API_KEY` | string | API 密钥 | +| `TEMPERATURE` | string | Chat Completions 采样温度,范围 `"0"` 到 `"2"` | +| `THINKING_ENABLED` | string | 是否启用思考模式 | +| `REASONING_EFFORT` | string | 推理强度 | +| `DEBUG_LOG_ENABLED` | string | 是否启用调试日志输出 | +| `TELEMETRY_ENABLED` | string | 是否启用匿名使用数据上报 | +| `<其他任意KEY>` | string | 自定义环境变量 | #### `thinkingEnabled` — 思考模式 @@ -63,10 +63,10 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 当思考模式启用时,控制模型思考的深度: -| 值 | 说明 | -| ------ | --------------------------------- | -| `max` | 最大推理深度(默认值) | -| `high` | 较高推理深度,token消耗相对较小 | +| 值 | 说明 | +| ------ | ------------------------------- | +| `max` | 最大推理深度(默认值) | +| `high` | 较高推理深度,token消耗相对较小 | #### `notify` — 任务完成通知 @@ -74,13 +74,13 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 通知脚本执行时,会通过环境变量注入以下上下文信息: -| 环境变量 | 说明 | -|----------|------| -| `DURATION` | 会话耗时,单位秒(整数) | -| `STATUS` | 会话状态:`"completed"` 或 `"failed"` | -| `FAIL_REASON` | 失败原因(仅失败时设置) | -| `BODY` | 最后一条 AI 助手回复的文本内容 | -| `TITLE` | 会话标题(对应 resume 列表中的标题) | +| 环境变量 | 说明 | +| ------------- | ------------------------------------- | +| `DURATION` | 会话耗时,单位秒(整数) | +| `STATUS` | 会话状态:`"completed"` 或 `"failed"` | +| `FAIL_REASON` | 失败原因(仅失败时设置) | +| `BODY` | 最后一条 AI 助手回复的文本内容 | +| `TITLE` | 会话标题(对应 resume 列表中的标题) | ```json { @@ -137,17 +137,16 @@ MCP(Model Context Protocol)服务器配置。值是键值对,键为服务 } ``` -| McpServerConfig 字段 | 类型 | 必填 | 说明 | -| -------------------- | -------- | ---- | -------------------------------------------------------------------- | -| `command` | string | 是 | 可执行文件路径或命令(如 `npx`、`node`、`python`) | -| `args` | string[] | 否 | 传递给命令的参数列表 | -| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量 | +| McpServerConfig 字段 | 类型 | 必填 | 说明 | +| -------------------- | -------- | ---- | -------------------------------------------------- | +| `command` | string | 是 | 可执行文件路径或命令(如 `npx`、`node`、`python`) | +| `args` | string[] | 否 | 传递给命令的参数列表 | +| `env` | object | 否 | 传递给 MCP 服务器进程的环境变量 | > 当 `command` 为 `npx` 时,Deep Code 会自动在参数前补充 `-y`。 详细 MCP 使用说明请参考 [mcp.md](mcp.md)。 - #### `debugLogEnabled` — 调试日志 设为 `true` 可让程序输出详细的调试日志(默认 `false`),用于排查 API 调用和工具执行的问题。 @@ -171,6 +170,7 @@ DEEPCODE_TELEMETRY_ENABLED=0 deepcode 环境变量优先级遵循“越具体、越局部的配置,优先级越高”和“env文件默认保护现有环境,系统变量高于env文件”的覆盖逻辑。(settings.json的env对象可以认为是一种env文件) 优先级层级 (由低到高) + 1. settings.json 外层的 env:这是针对整个工具及其所有子进程的通用配置(全局变量)。可被外层环境变量覆盖,但环境变量KEY会移除`DEEPCODE_`前缀。 2. settings.json mcpServers 内定义的 env:这是针对特定 MCP 服务的最具体配置(局部变量)。可被外层环境变量覆盖,但环境变量KEY会移除`MCP_`前缀。 3. Shell 环境系统变量:操作系统层面的环境变量。 diff --git a/templates/skills/bundled/deepcode-self-refer/references/configuration_en.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/configuration_en.md similarity index 71% rename from templates/skills/bundled/deepcode-self-refer/references/configuration_en.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/configuration_en.md index fac8c349..2c58b5d0 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/configuration_en.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/configuration_en.md @@ -4,53 +4,53 @@ Configuration is applied in the following priority order (lower-numbered sources are overridden by higher-numbered ones): -| Layer | Configuration Source | Description | -| ----- | -------------------- | ---------------------------------------------- | -| 1 | Defaults | Hardcoded defaults within the application | -| 2 | User settings file | Global settings for the current user | -| 3 | Project settings file| Project-specific settings | -| 4 | Environment variables| System-wide or session-specific variables | +| Layer | Configuration Source | Description | +| ----- | --------------------- | ----------------------------------------- | +| 1 | Defaults | Hardcoded defaults within the application | +| 2 | User settings file | Global settings for the current user | +| 3 | Project settings file | Project-specific settings | +| 4 | Environment variables | System-wide or session-specific variables | ## Settings File Deep Code uses the `settings.json` file for persistent configuration, supporting two storage locations: -| File Type | Location | Scope | -| ------------------- | ----------------------------------------- | --------------------------------------------------------------------- | -| User settings file | `~/.deepcode/settings.json` | Applies to all Deep Code sessions for the current user. | +| File Type | Location | Scope | +| --------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| User settings file | `~/.deepcode/settings.json` | Applies to all Deep Code sessions for the current user. | | Project settings file | `/.deepcode/settings.json` | Takes effect only when running Deep Code in that specific project. Project settings override user settings. | ### Available Settings in `settings.json` The following are all the top-level fields supported in `settings.json`, along with the sub-fields inside `env`: -| Field | Type | Description | -| ------------------ | ------- | --------------------------------------------------------------------------- | -| `env` | object | Group of environment variables (see sub-field table below) | -| `model` | string | Model name. Takes precedence over `env.MODEL` | -| `thinkingEnabled` | boolean | Whether to enable thinking mode (enabled by default for DeepSeek V4 series)| -| `reasoningEffort` | string | Reasoning intensity, either `"high"` or `"max"` (default `"max"`) | -| `debugLogEnabled` | boolean | Enable debug log output (default `false`) | -| `telemetryEnabled` | boolean | Enable anonymous usage reporting (default `true`) | -| `notify` | string | Full path to a task-completion notification script (e.g., Slack notification script) | -| `webSearchTool` | string | Full path to a custom web search script | +| Field | Type | Description | +| ------------------ | ------- | -------------------------------------------------------------------------------------- | +| `env` | object | Group of environment variables (see sub-field table below) | +| `model` | string | Model name. Takes precedence over `env.MODEL` | +| `thinkingEnabled` | boolean | Whether to enable thinking mode (enabled by default for DeepSeek V4 series) | +| `reasoningEffort` | string | Reasoning intensity, either `"high"` or `"max"` (default `"max"`) | +| `debugLogEnabled` | boolean | Enable debug log output (default `false`) | +| `telemetryEnabled` | boolean | Enable anonymous usage reporting (default `true`) | +| `notify` | string | Full path to a task-completion notification script (e.g., Slack notification script) | +| `webSearchTool` | string | Full path to a custom web search script | | `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) | -| `temperature` | number | Sampling temperature for LLM, from `0` to `2` | -| `enabledSkills` | object | Per-skill enable/disable map, keyed by skill name | +| `temperature` | number | Sampling temperature for LLM, from `0` to `2` | +| `enabledSkills` | object | Per-skill enable/disable map, keyed by skill name | #### `env` Sub-fields -| Field | Type | Description | -| ----------------- | ------ | ---------------------------------------------------------------- | -| `MODEL` | string | Model name, e.g. `"deepseek-v4-pro"`, `"deepseek-v4-flash"` | -| `BASE_URL` | string | Base URL for API requests, e.g. `"https://api.deepseek.com"` | -| `API_KEY` | string | API key | -| `TEMPERATURE` | string | Sampling temperature for chat completions, from `"0"` to `"2"` | -| `THINKING_ENABLED`| string | Enable thinking mode | -| `REASONING_EFFORT`| string | Reasoning intensity | -| `DEBUG_LOG_ENABLED`| string| Enable debug log output | -| `TELEMETRY_ENABLED`| string| Enable anonymous usage reporting | -| `` | string | Custom environment variable | +| Field | Type | Description | +| ------------------- | ------ | -------------------------------------------------------------- | +| `MODEL` | string | Model name, e.g. `"deepseek-v4-pro"`, `"deepseek-v4-flash"` | +| `BASE_URL` | string | Base URL for API requests, e.g. `"https://api.deepseek.com"` | +| `API_KEY` | string | API key | +| `TEMPERATURE` | string | Sampling temperature for chat completions, from `"0"` to `"2"` | +| `THINKING_ENABLED` | string | Enable thinking mode | +| `REASONING_EFFORT` | string | Reasoning intensity | +| `DEBUG_LOG_ENABLED` | string | Enable debug log output | +| `TELEMETRY_ENABLED` | string | Enable anonymous usage reporting | +| `` | string | Custom environment variable | #### `thinkingEnabled` — Thinking Mode @@ -63,10 +63,10 @@ Whether to enable DeepSeek thinking mode. Set to `true` to enable, `false` to di When thinking mode is enabled, controls the depth of the model’s reasoning: -| Value | Description | -| ------ | --------------------------------------------------------- | -| `max` | Maximum reasoning depth (default) | -| `high` | Higher reasoning depth with relatively lower token usage | +| Value | Description | +| ------ | -------------------------------------------------------- | +| `max` | Maximum reasoning depth (default) | +| `high` | Higher reasoning depth with relatively lower token usage | #### `notify` — Task Completion Notification @@ -74,13 +74,13 @@ Set a full path to a shell script. When the AI assistant finishes a round of tas The following context is injected as environment variables when the notify script runs: -| Variable | Description | -|----------|-------------| -| `DURATION` | Session duration in seconds (integer) | -| `STATUS` | Session status: `"completed"` or `"failed"` | -| `FAIL_REASON` | Failure reason (only set on failure) | -| `BODY` | The text content of the last AI assistant reply | -| `TITLE` | Session title (matches the resume list title) | +| Variable | Description | +| ------------- | ----------------------------------------------- | +| `DURATION` | Session duration in seconds (integer) | +| `STATUS` | Session status: `"completed"` or `"failed"` | +| `FAIL_REASON` | Failure reason (only set on failure) | +| `BODY` | The text content of the last AI assistant reply | +| `TITLE` | Session title (matches the resume list title) | ```json { @@ -137,11 +137,11 @@ Configuration for MCP (Model Context Protocol) servers. The value is a key-value } ``` -| McpServerConfig field | Type | Required | Description | -| --------------------- | -------- | -------- | ------------------------------------------------------------------------ | -| `command` | string | Yes | Executable path or command (e.g. `npx`, `node`, `python`) | -| `args` | string[] | No | List of arguments passed to the command | -| `env` | object | No | Environment variables passed to the MCP server process | +| McpServerConfig field | Type | Required | Description | +| --------------------- | -------- | -------- | --------------------------------------------------------- | +| `command` | string | Yes | Executable path or command (e.g. `npx`, `node`, `python`) | +| `args` | string[] | No | List of arguments passed to the command | +| `env` | object | No | Environment variables passed to the MCP server process | > When `command` is `npx`, Deep Code automatically prepends `-y` to the arguments. @@ -170,6 +170,7 @@ Environment variables are a common way to configure applications, especially for Environment variable priority follows the logic of “the more specific and localized the configuration, the higher the priority”, and the override rule of “env files protect existing environment by default, system variables override env files”. (The `env` object in settings.json can be thought of as a type of env file.) Priority levels (from lowest to highest): + 1. `env` defined at the top level of `settings.json` – this is a general configuration for the entire tool and all its subprocesses (global variables). Can be overridden by outer environment variables, but the environment variable KEY has the `DEEPCODE_` prefix removed. 2. `env` defined inside `mcpServers` in `settings.json` – this is the most specific configuration for a particular MCP service (local variables). Can be overridden by outer environment variables, but the KEY has the `MCP_` prefix removed. 3. Shell/system environment variables – operating system level. diff --git a/templates/skills/bundled/deepcode-self-refer/references/mcp.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/mcp.md similarity index 100% rename from templates/skills/bundled/deepcode-self-refer/references/mcp.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/mcp.md diff --git a/templates/skills/bundled/deepcode-self-refer/references/mcp_en.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/mcp_en.md similarity index 96% rename from templates/skills/bundled/deepcode-self-refer/references/mcp_en.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/mcp_en.md index 03c4b30c..7933db6a 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/mcp_en.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/mcp_en.md @@ -41,11 +41,11 @@ Edit `~/.deepcode/settings.json` and add the `mcpServers` field: ### Configuration Fields -| Field | Type | Required | Description | -| --------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Field | Type | Required | Description | +| --------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `command` | string | Yes | Path or command of the MCP server executable (e.g., `npx`, `node`, `python`). When the command is `npx`, Deep Code automatically prepends `-y` to the arguments. | -| `args` | string[] | No | List of arguments to pass to the command | -| `env` | object | No | Environment variables (e.g., API keys) to pass to the MCP server process | +| `args` | string[] | No | List of arguments to pass to the command | +| `env` | object | No | Environment variables (e.g., API keys) to pass to the MCP server process | ## Common MCP Examples @@ -160,12 +160,12 @@ The AI will automatically invoke the `mcp__github__search_issues` tool to comple An MCP tool name consists of three parts: `mcp____` -| Service | Tool Name | Full Invocation Name | -| ---------- | ----------------------- | ------------------------------------------- | -| github | search_code | `mcp__github__search_code` | -| github | create_pull_request | `mcp__github__create_pull_request` | -| playwright | browser_navigate | `mcp__playwright__browser_navigate` | -| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` | +| Service | Tool Name | Full Invocation Name | +| ---------- | ----------------------- | ------------------------------------------ | +| github | search_code | `mcp__github__search_code` | +| github | create_pull_request | `mcp__github__create_pull_request` | +| playwright | browser_navigate | `mcp__playwright__browser_navigate` | +| playwright | browser_take_screenshot | `mcp__playwright__browser_take_screenshot` | You can view the list of tools provided by each server using `/mcp`. @@ -197,4 +197,4 @@ MCP servers follow the [Model Context Protocol](https://modelcontextprotocol.io/ 2. `tools/list` — Return the list of available tools 3. `tools/call` — Execute a tool call -For more information, see the [official MCP documentation](https://modelcontextprotocol.io/). \ No newline at end of file +For more information, see the [official MCP documentation](https://modelcontextprotocol.io/). diff --git a/templates/skills/bundled/deepcode-self-refer/references/notify.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/notify.md similarity index 91% rename from templates/skills/bundled/deepcode-self-refer/references/notify.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/notify.md index d73eef45..553722f3 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/notify.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/notify.md @@ -8,13 +8,13 @@ ## 注入的环境变量 -| 环境变量 | 说明 | -|----------|------| -| `DURATION` | 会话耗时,单位秒(整数) | -| `STATUS` | 会话状态:`"completed"` 或 `"failed"` | -| `FAIL_REASON` | 失败原因(仅失败时设置) | -| `BODY` | 最后一条 AI 助手回复的文本内容 | -| `TITLE` | 会话标题(对应 resume 列表中的标题) | +| 环境变量 | 说明 | +| ------------- | ------------------------------------- | +| `DURATION` | 会话耗时,单位秒(整数) | +| `STATUS` | 会话状态:`"completed"` 或 `"failed"` | +| `FAIL_REASON` | 失败原因(仅失败时设置) | +| `BODY` | 最后一条 AI 助手回复的文本内容 | +| `TITLE` | 会话标题(对应 resume 列表中的标题) | ## 配置方法 diff --git a/templates/skills/bundled/deepcode-self-refer/references/notify_en.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/notify_en.md similarity index 91% rename from templates/skills/bundled/deepcode-self-refer/references/notify_en.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/notify_en.md index b949161c..90fcf706 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/notify_en.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/notify_en.md @@ -8,13 +8,13 @@ Configure the `notify` field in `settings.json` with the full path to an executa ## Injected Environment Variables -| Variable | Description | -|----------|-------------| -| `DURATION` | Session duration in seconds (integer) | -| `STATUS` | Session status: `"completed"` or `"failed"` | -| `FAIL_REASON` | Failure reason (only set on failure) | -| `BODY` | The text content of the last AI assistant reply | -| `TITLE` | Session title (matches the resume list title) | +| Variable | Description | +| ------------- | ----------------------------------------------- | +| `DURATION` | Session duration in seconds (integer) | +| `STATUS` | Session status: `"completed"` or `"failed"` | +| `FAIL_REASON` | Failure reason (only set on failure) | +| `BODY` | The text content of the last AI assistant reply | +| `TITLE` | Session title (matches the resume list title) | ## Configuration diff --git a/templates/skills/bundled/deepcode-self-refer/references/permission.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/permission.md similarity index 61% rename from templates/skills/bundled/deepcode-self-refer/references/permission.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/permission.md index 91c19c6f..e315c47c 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/permission.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/permission.md @@ -14,18 +14,18 @@ Deep Code 内置了一套细粒度的权限控制机制,在 AI 助手执行工 Deep Code 定义了以下 10 种权限范围,覆盖了工具调用的各类风险场景: -| 权限范围 | 说明 | -| -------- | ---- | -| `read-in-cwd` | 读取当前工作区内的文件 | -| `read-out-cwd` | 读取当前工作区外的文件 | -| `write-in-cwd` | 在当前工作区内创建或覆写文件 | -| `write-out-cwd` | 在当前工作区外创建或覆写文件 | -| `delete-in-cwd` | 删除当前工作区内的文件 | -| `delete-out-cwd` | 删除当前工作区外的文件 | -| `query-git-log` | 查询 Git 历史(如 `git log`、`git show`、`git blame`) | +| 权限范围 | 说明 | +| ---------------- | --------------------------------------------------------- | +| `read-in-cwd` | 读取当前工作区内的文件 | +| `read-out-cwd` | 读取当前工作区外的文件 | +| `write-in-cwd` | 在当前工作区内创建或覆写文件 | +| `write-out-cwd` | 在当前工作区外创建或覆写文件 | +| `delete-in-cwd` | 删除当前工作区内的文件 | +| `delete-out-cwd` | 删除当前工作区外的文件 | +| `query-git-log` | 查询 Git 历史(如 `git log`、`git show`、`git blame`) | | `mutate-git-log` | 修改 Git 历史(如 `git commit`、`git rebase`、`git tag`) | -| `network` | 访问网络(如 `curl`、`npm install` 等联网操作) | -| `mcp` | 调用 MCP 外部工具 | +| `network` | 访问网络(如 `curl`、`npm install` 等联网操作) | +| `mcp` | 调用 MCP 外部工具 | 此外还有一个特殊的 `unknown` 范围,当 LLM 无法准确分类命令的副作用时使用,**`unknown` 总是触发询问**。 @@ -46,11 +46,11 @@ Deep Code 定义了以下 10 种权限范围,覆盖了工具调用的各类风 ### 配置项说明 -| 字段 | 类型 | 说明 | -| ---- | ---- | ---- | -| `allow` | `string[]` | 始终自动放行的权限范围列表 | -| `deny` | `string[]` | 始终自动拒绝的权限范围列表 | -| `ask` | `string[]` | 始终弹出询问的权限范围列表 | +| 字段 | 类型 | 说明 | +| ------------- | -------------------------- | --------------------------------------------------------------------------------- | +| `allow` | `string[]` | 始终自动放行的权限范围列表 | +| `deny` | `string[]` | 始终自动拒绝的权限范围列表 | +| `ask` | `string[]` | 始终弹出询问的权限范围列表 | | `defaultMode` | `"allowAll"` \| `"askAll"` | 未在 `allow`/`deny`/`ask` 中明确列出的权限范围的默认处理方式。默认为 `"allowAll"` | ### 优先级规则 @@ -86,10 +86,10 @@ Deep Code 定义了以下 10 种权限范围,覆盖了工具调用的各类风 ``` 此配置的效果: + - 工作区内读写、Git 查询 → 自动放行 - 其他操作都需要用户确认。 - ## 持久化机制 当用户在权限提示中选择 "Yes, and always allow" 后,对应的权限范围会被写入当前项目的 `.deepcode/settings.json` 文件中: diff --git a/templates/skills/bundled/deepcode-self-refer/references/permission_en.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/permission_en.md similarity index 64% rename from templates/skills/bundled/deepcode-self-refer/references/permission_en.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/permission_en.md index dae739c0..1298a1d6 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/permission_en.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/permission_en.md @@ -14,18 +14,18 @@ Each time the AI assistant invokes a tool, the system automatically analyzes the Deep Code defines the following 10 permission scopes, covering various risk scenarios for tool calls: -| Permission Scope | Description | -| ---------------- | ----------- | -| `read-in-cwd` | Read files inside the current workspace | -| `read-out-cwd` | Read files outside the current workspace | -| `write-in-cwd` | Create or overwrite files inside the current workspace | -| `write-out-cwd` | Create or overwrite files outside the current workspace | -| `delete-in-cwd` | Delete files inside the current workspace | -| `delete-out-cwd` | Delete files outside the current workspace | -| `query-git-log` | Query Git history (e.g., `git log`, `git show`, `git blame`) | +| Permission Scope | Description | +| ---------------- | ---------------------------------------------------------------- | +| `read-in-cwd` | Read files inside the current workspace | +| `read-out-cwd` | Read files outside the current workspace | +| `write-in-cwd` | Create or overwrite files inside the current workspace | +| `write-out-cwd` | Create or overwrite files outside the current workspace | +| `delete-in-cwd` | Delete files inside the current workspace | +| `delete-out-cwd` | Delete files outside the current workspace | +| `query-git-log` | Query Git history (e.g., `git log`, `git show`, `git blame`) | | `mutate-git-log` | Mutate Git history (e.g., `git commit`, `git rebase`, `git tag`) | -| `network` | Access the network (e.g., `curl`, `npm install`) | -| `mcp` | Invoke MCP external tools | +| `network` | Access the network (e.g., `curl`, `npm install`) | +| `mcp` | Invoke MCP external tools | There is also a special `unknown` scope used when the LLM cannot classify a command's side effects — **`unknown` always triggers a prompt**. @@ -46,11 +46,11 @@ Configure permissions in `~/.deepcode/settings.json` (user-level) or `.deepcode/ ### Configuration Fields -| Field | Type | Description | -| ----- | ---- | ----------- | -| `allow` | `string[]` | Permission scopes that are always auto-allowed | -| `deny` | `string[]` | Permission scopes that are always auto-denied | -| `ask` | `string[]` | Permission scopes that always trigger a confirmation prompt | +| Field | Type | Description | +| ------------- | -------------------------- | --------------------------------------------------------------------------------------------------- | +| `allow` | `string[]` | Permission scopes that are always auto-allowed | +| `deny` | `string[]` | Permission scopes that are always auto-denied | +| `ask` | `string[]` | Permission scopes that always trigger a confirmation prompt | | `defaultMode` | `"allowAll"` \| `"askAll"` | Default behavior for scopes not explicitly listed in `allow`/`deny`/`ask`. Defaults to `"allowAll"` | ### Priority Rules @@ -86,6 +86,7 @@ Default behavior: all operations are auto-allowed with no confirmation required. ``` With this configuration: + - Reading/writing inside the workspace and querying Git history → auto-allowed - All other operations → require user confirmation diff --git a/templates/skills/bundled/deepcode-self-refer/references/session-persistence.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/session-persistence.md similarity index 75% rename from templates/skills/bundled/deepcode-self-refer/references/session-persistence.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/session-persistence.md index 835d2881..1c629e4d 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/session-persistence.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/session-persistence.md @@ -14,11 +14,11 @@ Deep Code 会把每个项目的会话记录保存在本机用户目录中。会 项目存储目录包含以下主要文件和目录: -| 路径 | 说明 | -| ---- | ---- | +| 路径 | 说明 | +| --------------------- | ------------------------------------------------------ | | `sessions-index.json` | 当前项目的会话索引,保存会话列表和每个会话的概要信息。 | -| `.jsonl` | 单个会话的消息记录。每一行是一条 JSON 格式的消息。 | -| `file-history/.git` | 用于代码快照的内部 Git 仓库,供 `/undo` 恢复文件内容。 | +| `.jsonl` | 单个会话的消息记录。每一行是一条 JSON 格式的消息。 | +| `file-history/.git` | 用于代码快照的内部 Git 仓库,供 `/undo` 恢复文件内容。 | ## 持久化内容 @@ -38,18 +38,18 @@ Deep Code 会把每个项目的会话记录保存在本机用户目录中。会 每个会话有一个独立的 JSONL 消息文件,文件名是 `.jsonl`。消息按追加顺序写入,常见字段包括: -| 字段 | 说明 | -| ---- | ---- | -| `id` | 消息 ID。 | -| `sessionId` | 所属会话 ID。 | -| `role` | 消息角色:`system`、`user`、`assistant` 或 `tool`。 | -| `content` | 文本内容。 | -| `contentParams` | 结构化内容,例如图片输入。 | -| `messageParams` | 模型消息参数,例如 tool call ID、tool calls、reasoning content。 | -| `visible` | 是否在界面中显示。 | -| `compacted` | 是否已经被长会话压缩替代。 | -| `checkpointHash` | 与 `/undo` 关联的代码快照哈希。 | -| `meta` | 工具展示、skill、权限、摘要等附加信息。 | +| 字段 | 说明 | +| ---------------- | ---------------------------------------------------------------- | +| `id` | 消息 ID。 | +| `sessionId` | 所属会话 ID。 | +| `role` | 消息角色:`system`、`user`、`assistant` 或 `tool`。 | +| `content` | 文本内容。 | +| `contentParams` | 结构化内容,例如图片输入。 | +| `messageParams` | 模型消息参数,例如 tool call ID、tool calls、reasoning content。 | +| `visible` | 是否在界面中显示。 | +| `compacted` | 是否已经被长会话压缩替代。 | +| `checkpointHash` | 与 `/undo` 关联的代码快照哈希。 | +| `meta` | 工具展示、skill、权限、摘要等附加信息。 | 读取消息文件时,Deep Code 会逐行解析 JSON;无法解析的行会被忽略,以便尽量保留其余可用历史。 @@ -113,11 +113,11 @@ Deep Code 使用 `file-history/.git` 保存代码快照。这个仓库只作为 根据选择,Deep Code 可以执行以下操作: -| 操作 | 行为 | -| ---- | ---- | +| 操作 | 行为 | +| -------- | ------------------------------------------------------------------- | | 恢复对话 | 截断所选用户消息之前的消息历史,并更新索引中的最新 assistant 信息。 | -| 恢复代码 | 从 `file-history/.git` 中读取所选快照,并还原被跟踪文件。 | -| 同时恢复 | 先恢复代码,再截断对话历史。 | +| 恢复代码 | 从 `file-history/.git` 中读取所选快照,并还原被跟踪文件。 | +| 同时恢复 | 先恢复代码,再截断对话历史。 | 恢复对话会重写该会话的 JSONL 文件;恢复代码会修改工作区中被快照跟踪的文件。 diff --git a/templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md similarity index 72% rename from templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md rename to packages/core/templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md index 071a5353..865bf04b 100644 --- a/templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/session-persistence_en.md @@ -14,11 +14,11 @@ Each project has its own storage directory: The project storage directory contains these main files and directories: -| Path | Description | -| ---- | ----------- | +| Path | Description | +| --------------------- | --------------------------------------------------------------------------------------- | | `sessions-index.json` | Session index for the current project, including the session list and summary metadata. | -| `.jsonl` | Message log for one session. Each line is one JSON message. | -| `file-history/.git` | Internal Git repository used for code checkpoints restored by `/undo`. | +| `.jsonl` | Message log for one session. Each line is one JSON message. | +| `file-history/.git` | Internal Git repository used for code checkpoints restored by `/undo`. | ## Persisted Data @@ -38,18 +38,18 @@ The default session title comes from the first 100 characters of the first user Each session has a separate JSONL message file named `.jsonl`. Messages are appended in order. Common fields include: -| Field | Description | -| ----- | ----------- | -| `id` | Message ID. | -| `sessionId` | Owning session ID. | -| `role` | Message role: `system`, `user`, `assistant`, or `tool`. | -| `content` | Text content. | -| `contentParams` | Structured content, such as image input. | -| `messageParams` | Model message parameters, such as tool call IDs, tool calls, and reasoning content. | -| `visible` | Whether the message is shown in the UI. | -| `compacted` | Whether the message has been replaced by long-session compaction. | -| `checkpointHash` | Code checkpoint hash associated with `/undo`. | -| `meta` | Extra metadata for tool display, skills, permissions, summaries, and related features. | +| Field | Description | +| ---------------- | -------------------------------------------------------------------------------------- | +| `id` | Message ID. | +| `sessionId` | Owning session ID. | +| `role` | Message role: `system`, `user`, `assistant`, or `tool`. | +| `content` | Text content. | +| `contentParams` | Structured content, such as image input. | +| `messageParams` | Model message parameters, such as tool call IDs, tool calls, and reasoning content. | +| `visible` | Whether the message is shown in the UI. | +| `compacted` | Whether the message has been replaced by long-session compaction. | +| `checkpointHash` | Code checkpoint hash associated with `/undo`. | +| `meta` | Extra metadata for tool display, skills, permissions, summaries, and related features. | When loading a message file, Deep Code parses JSON one line at a time. Malformed lines are ignored so the remaining usable history can still be loaded. @@ -113,11 +113,11 @@ These states are persisted in `sessions-index.json`, so they remain visible in t Depending on the selected mode, Deep Code can perform these operations: -| Operation | Behavior | -| --------- | -------- | +| Operation | Behavior | +| -------------------- | -------------------------------------------------------------------------------------------------------------- | | Restore conversation | Truncates message history before the selected user message and updates the latest assistant data in the index. | -| Restore code | Reads the selected checkpoint from `file-history/.git` and restores tracked files. | -| Restore both | Restores code first, then truncates the conversation history. | +| Restore code | Reads the selected checkpoint from `file-history/.git` and restores tracked files. | +| Restore both | Restores code first, then truncates the conversation history. | Restoring conversation rewrites the session JSONL file. Restoring code modifies workspace files tracked by the selected checkpoint. diff --git a/templates/skills/bundled/plan/SKILL.md b/packages/core/templates/skills/bundled/plan/SKILL.md similarity index 78% rename from templates/skills/bundled/plan/SKILL.md rename to packages/core/templates/skills/bundled/plan/SKILL.md index b73c1abc..41c43013 100644 --- a/templates/skills/bundled/plan/SKILL.md +++ b/packages/core/templates/skills/bundled/plan/SKILL.md @@ -5,7 +5,7 @@ description: Plan tasks through a strict non-mutating collaboration workflow bef # Plan Mode (Conversational) -You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed—intent- and implementation-wise—so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions. +You work in 3 phases, and you should _chat your way_ to a great plan before finalizing it. A great plan is very detailed—intent- and implementation-wise—so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions. ## Mode rules (strict) @@ -27,19 +27,19 @@ You may explore and execute **non-mutating** actions that improve the plan. You Actions that gather truth, reduce ambiguity, or validate feasibility without changing repo-tracked state. Examples: -* Reading or searching files, configs, schemas, types, manifests, and docs -* Static analysis, inspection, and repo exploration -* Dry-run style commands when they do not edit repo-tracked files -* Tests, builds, or checks that may write to caches or build artifacts (for example, `target/`, `.cache/`, or snapshots) so long as they do not edit repo-tracked files +- Reading or searching files, configs, schemas, types, manifests, and docs +- Static analysis, inspection, and repo exploration +- Dry-run style commands when they do not edit repo-tracked files +- Tests, builds, or checks that may write to caches or build artifacts (for example, `target/`, `.cache/`, or snapshots) so long as they do not edit repo-tracked files ### Not allowed (mutating, plan-executing) Actions that implement the plan or change repo-tracked state. Examples: -* Editing or writing files -* Running formatters or linters that rewrite files -* Applying patches, migrations, or codegen that updates repo-tracked files -* Side-effectful commands whose purpose is to carry out the plan rather than refine it +- Editing or writing files +- Running formatters or linters that rewrite files +- Applying patches, migrations, or codegen that updates repo-tracked files +- Side-effectful commands whose purpose is to carry out the plan rather than refine it When in doubt: if the action would reasonably be described as "doing the work" rather than "planning the work," do not do it. @@ -55,27 +55,27 @@ Do not ask questions that can be answered from the repo or system (for example, ## PHASE 2 — Intent chat (what they actually want) -* Keep asking until you can clearly state: goal + success criteria, audience, in/out of scope, constraints, current state, and the key preferences/tradeoffs. -* Bias toward questions over guessing: if any high-impact ambiguity remains, do NOT plan yet—ask. +- Keep asking until you can clearly state: goal + success criteria, audience, in/out of scope, constraints, current state, and the key preferences/tradeoffs. +- Bias toward questions over guessing: if any high-impact ambiguity remains, do NOT plan yet—ask. ## PHASE 3 — Implementation chat (what/how we’ll build) -* Once intent is stable, keep asking until the spec is decision complete: approach, interfaces (APIs/schemas/I/O), data flow, edge cases/failure modes, testing + acceptance criteria, rollout/monitoring, and any migrations/compat constraints. +- Once intent is stable, keep asking until the spec is decision complete: approach, interfaces (APIs/schemas/I/O), data flow, edge cases/failure modes, testing + acceptance criteria, rollout/monitoring, and any migrations/compat constraints. ## Asking questions Critical rules: -* Strongly prefer using the `AskUserQuestion` tool to ask any questions. -* Offer only meaningful multiple‑choice options; don’t include filler choices that are obviously wrong or irrelevant. -* In rare cases where an unavoidable, important question can’t be expressed with reasonable multiple‑choice options (due to extreme ambiguity), you may ask it directly without the tool. +- Strongly prefer using the `AskUserQuestion` tool to ask any questions. +- Offer only meaningful multiple‑choice options; don’t include filler choices that are obviously wrong or irrelevant. +- In rare cases where an unavoidable, important question can’t be expressed with reasonable multiple‑choice options (due to extreme ambiguity), you may ask it directly without the tool. You SHOULD ask many questions, but each question must: -* materially change the spec/plan, OR -* confirm/lock an assumption, OR -* choose between meaningful tradeoffs. -* not be answerable by non-mutating commands. +- materially change the spec/plan, OR +- confirm/lock an assumption, OR +- choose between meaningful tradeoffs. +- not be answerable by non-mutating commands. Use the `AskUserQuestion` tool only for decisions that materially change the plan, for confirming important assumptions, or for information that cannot be discovered via non-mutating exploration. Ask one question at a time when possible, provide concrete options with `label` and optional `description`, and use `multiSelect` only when multiple choices can be combined. @@ -83,16 +83,16 @@ Use the `AskUserQuestion` tool only for decisions that materially change the pla 1. **Discoverable facts** (repo/system truth): explore first. - * Before asking, run targeted searches and check likely sources of truth (configs/manifests/entrypoints/schemas/types/constants). - * Ask only if: multiple plausible candidates; nothing found but you need a missing identifier/context; or ambiguity is actually product intent. - * If asking, present concrete candidates (paths/service names) + recommend one. - * Never ask questions you can answer from your environment (e.g., “where is this struct”). + - Before asking, run targeted searches and check likely sources of truth (configs/manifests/entrypoints/schemas/types/constants). + - Ask only if: multiple plausible candidates; nothing found but you need a missing identifier/context; or ambiguity is actually product intent. + - If asking, present concrete candidates (paths/service names) + recommend one. + - Never ask questions you can answer from your environment (e.g., “where is this struct”). 2. **Preferences/tradeoffs** (not discoverable): ask early. - * These are intent or implementation preferences that cannot be derived from exploration. - * Provide 2–4 mutually exclusive options + a recommended default. - * If unanswered, proceed with the recommended option and record it as an assumption in the final plan. + - These are intent or implementation preferences that cannot be derived from exploration. + - Provide 2–4 mutually exclusive options + a recommended default. + - If unanswered, proceed with the recommended option and record it as an assumption in the final plan. ## Finalization rule @@ -100,11 +100,11 @@ Only output the final plan when it is decision complete and leaves no decisions When you present the official plan, wrap it in a `` block so the client can render it specially: -1) The opening tag must be on its own line. -2) Start the plan content on the next line (no text on the same line as the tag). -3) The closing tag must be on its own line. -4) Use Markdown inside the block. -5) Keep the tags exactly as `` and `` (do not translate or rename them), even if the plan content is in another language. +1. The opening tag must be on its own line. +2. Start the plan content on the next line (no text on the same line as the tag). +3. The closing tag must be on its own line. +4. Use Markdown inside the block. +5. Keep the tags exactly as `` and `` (do not translate or rename them), even if the plan content is in another language. Example: @@ -114,11 +114,11 @@ plan content plan content should be human and agent digestible. The final plan must be plan-only, concise by default, and include: -* A clear title -* A brief summary section -* Important changes or additions to public APIs/interfaces/types -* Test cases and scenarios -* Explicit assumptions and defaults chosen where needed +- A clear title +- A brief summary section +- Important changes or additions to public APIs/interfaces/types +- Test cases and scenarios +- Explicit assumptions and defaults chosen where needed When possible, prefer a compact structure with 3-5 short sections, usually: Summary, Key Changes or Implementation Changes, Test Plan, and Assumptions. Do not include a separate Scope section unless scope boundaries are genuinely important to avoid mistakes. diff --git a/templates/skills/bundled/skill-digester/SKILL.md b/packages/core/templates/skills/bundled/skill-digester/SKILL.md similarity index 89% rename from templates/skills/bundled/skill-digester/SKILL.md rename to packages/core/templates/skills/bundled/skill-digester/SKILL.md index 6e9016d1..3bef806b 100644 --- a/templates/skills/bundled/skill-digester/SKILL.md +++ b/packages/core/templates/skills/bundled/skill-digester/SKILL.md @@ -22,6 +22,7 @@ Whenever user input is needed, call the `AskUserQuestion` tool. Do not ask follo ``` If this skill is loaded from a project-level or different user-level path, use the `scripts/find-skill.js` file next to this `SKILL.md` instead. + - The script searches the same roots Deep Code CLI scans, in priority order: 1. Project native skills: `./.deepcode/skills//SKILL.md` 2. Project interoperable skills: `./.agents/skills//SKILL.md` @@ -89,11 +90,35 @@ Use one question at a time unless two decisions are tightly coupled. Each questi Examples: ```json -{"questions":[{"question":"请选择您偏好的语言。","options":[{"label":"中文","description":"后续询问和推荐描述都使用中文。"},{"label":"English","description":"Use English for follow-up questions and the recommended description."}]}]} +{ + "questions": [ + { + "question": "请选择您偏好的语言。", + "options": [ + { "label": "中文", "description": "后续询问和推荐描述都使用中文。" }, + { "label": "English", "description": "Use English for follow-up questions and the recommended description." } + ] + } + ] +} ``` ```json -{"questions":[{"question":"How should I proceed with this description recommendation?","options":[{"label":"Apply change","description":"Update only the description field in the native digest output SKILL.md."},{"label":"Abandon change","description":"Leave the file unchanged."},{"label":"Discuss wording","description":"Continue refining the proposed description before editing."}]}]} +{ + "questions": [ + { + "question": "How should I proceed with this description recommendation?", + "options": [ + { + "label": "Apply change", + "description": "Update only the description field in the native digest output SKILL.md." + }, + { "label": "Abandon change", "description": "Leave the file unchanged." }, + { "label": "Discuss wording", "description": "Continue refining the proposed description before editing." } + ] + } + ] +} ``` ## Review Heuristics @@ -112,4 +137,3 @@ Avoid descriptions that are only generic labels, marketing copy, or internal imp - Never save the digested output under `.agents/skills`; `.agents/skills` is only a source root for digestion. - Never move a skill between project and user level during digestion. - Never change the target skill's language preference after confirmation unless the user asks. - diff --git a/templates/skills/bundled/skill-digester/scripts/find-skill.js b/packages/core/templates/skills/bundled/skill-digester/scripts/find-skill.js similarity index 100% rename from templates/skills/bundled/skill-digester/scripts/find-skill.js rename to packages/core/templates/skills/bundled/skill-digester/scripts/find-skill.js diff --git a/templates/skills/bundled/skill-writer/SKILL.md b/packages/core/templates/skills/bundled/skill-writer/SKILL.md similarity index 99% rename from templates/skills/bundled/skill-writer/SKILL.md rename to packages/core/templates/skills/bundled/skill-writer/SKILL.md index 1a7801c3..ca2adc41 100644 --- a/templates/skills/bundled/skill-writer/SKILL.md +++ b/packages/core/templates/skills/bundled/skill-writer/SKILL.md @@ -10,6 +10,7 @@ This Skill helps you create well-structured Agent Skills for AI agents that foll ## When to use this Skill Use this Skill when: + - Creating a new Agent Skill - Writing or updating SKILL.md files - Designing skill structure and frontmatter @@ -37,11 +38,13 @@ First, understand what the Skill should do: Determine where to create the Skill: **Personal Skills** (`~/.agents/skills/`): + - Individual workflows and preferences - Experimental Skills - Personal productivity tools **Project Skills** (`.agents/skills/`): + - Team workflows and conventions - Project-specific expertise - Shared utilities (committed to git) @@ -59,6 +62,7 @@ mkdir -p .agents/skills/skill-name ``` For multi-file Skills: + ``` skill-name/ ├── SKILL.md (required) @@ -116,22 +120,26 @@ The description is critical for AI agents to discover your Skill. **Examples**: ✅ **Good**: + ```yaml description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction. ``` ✅ **Good**: + ```yaml description: Analyze Excel spreadsheets, create pivot tables, and generate charts. Use when working with Excel files, spreadsheets, or analyzing tabular data in .xlsx format. ``` ❌ **Too vague**: + ```yaml description: Helps with documents description: For data analysis ``` **Tips**: + - Include specific file extensions (.pdf, .xlsx, .json) - Mention common user phrases ("analyze", "extract", "generate") - List concrete operations (not generic verbs) @@ -141,7 +149,7 @@ description: For data analysis Use clear Markdown sections: -```markdown +````markdown # Skill Name Brief overview of what this Skill does. @@ -153,6 +161,7 @@ Provide a simple example to get started immediately. ## Instructions Step-by-step guidance for AI agents: + 1. First step with clear action 2. Second step with expected outcome 3. Handle edge cases @@ -170,14 +179,17 @@ Show concrete usage examples with code or commands. ## Requirements List any dependencies or prerequisites: + ```bash pip install package-name ``` +```` ## Advanced usage For complex scenarios, see [reference.md](reference.md). -``` + +```` ### Step 7: Add supporting files (optional) @@ -196,17 +208,19 @@ Run the helper script: \`\`\`bash python scripts/helper.py input.txt \`\`\` -``` +```` ### Step 8: Validate the Skill Check these requirements: ✅ **File structure**: + - [ ] SKILL.md exists in correct location - [ ] Directory name matches frontmatter `name` ✅ **YAML frontmatter**: + - [ ] Opening `---` on line 1 - [ ] Closing `---` before content - [ ] Valid YAML (no tabs, correct indentation) @@ -214,12 +228,14 @@ Check these requirements: - [ ] `description` is specific and < 1024 chars ✅ **Content quality**: + - [ ] Clear instructions for AI agents - [ ] Concrete examples provided - [ ] Edge cases handled - [ ] Dependencies listed (if any) ✅ **Testing**: + - [ ] Description matches user questions - [ ] Skill activates on relevant queries - [ ] Instructions are clear and actionable @@ -229,6 +245,7 @@ Check these requirements: 1. **Restart AI agents** (if running) to load the Skill 2. **Ask relevant questions** that match the description: + ``` Can you help me extract text from this PDF? ``` @@ -247,6 +264,7 @@ If AI agents doesn't use the Skill: - Mention common user phrases 2. **Check file location**: + ```bash ls ~/.agents/skills/skill-name/SKILL.md ls .agents/skills/skill-name/SKILL.md @@ -343,16 +361,19 @@ Before finalizing a Skill, verify: ## Troubleshooting **Skill doesn't activate**: + - Make description more specific with trigger words - Include file types and operations in description - Add "Use when..." clause with user phrases **Multiple Skills conflict**: + - Make descriptions more distinct - Use different trigger words - Narrow the scope of each Skill **Skill has errors**: + - Check YAML syntax (no tabs, proper indentation) - Verify file paths (use forward slashes) - Ensure scripts have execute permissions @@ -361,6 +382,7 @@ Before finalizing a Skill, verify: ## Examples See the documentation for complete examples: + - Simple single-file Skill (commit-helper) - Skill with tool permissions (code-reviewer) - Multi-file Skill (pdf-processing) @@ -378,4 +400,3 @@ When creating a Skill, I will: 7. Validate against all requirements The result will be a complete, working Skill that follows all best practices and validation rules. - diff --git a/templates/skills/karpathy-guidelines.md b/packages/core/templates/skills/karpathy-guidelines.md similarity index 97% rename from templates/skills/karpathy-guidelines.md rename to packages/core/templates/skills/karpathy-guidelines.md index 41e23d73..8f5d86bf 100644 --- a/templates/skills/karpathy-guidelines.md +++ b/packages/core/templates/skills/karpathy-guidelines.md @@ -14,6 +14,7 @@ Behavioral guidelines to reduce common LLM coding mistakes. **Don't assume. Don't hide confusion. Surface tradeoffs.** Before implementing: + - State your assumptions explicitly. If uncertain, ask. - If multiple interpretations exist, present them - don't pick silently. - If a simpler approach exists, say so. Push back when warranted. @@ -36,12 +37,14 @@ Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, sim **Touch only what you must. Clean up only your own mess.** When editing existing code: + - Don't "improve" adjacent code, comments, or formatting. - Don't refactor things that aren't broken. - Match existing style, even if you'd do it differently. - If you notice unrelated dead code, mention it - don't delete it. When your changes create orphans: + - Remove imports/variables/functions that YOUR changes made unused. - Don't remove pre-existing dead code unless asked. @@ -52,15 +55,17 @@ The test: Every changed line should trace directly to the user's request. **Define success criteria. Loop until verified.** Transform tasks into verifiable goals: + - "Add validation" → "Write tests for invalid inputs, then make them pass" - "Fix the bug" → "Write a test that reproduces it, then make it pass" - "Refactor X" → "Ensure tests pass before and after" For multi-step tasks, state a brief plan: + ``` 1. [Step] → verify: [check] 2. [Step] → verify: [check] 3. [Step] → verify: [check] ``` -Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. \ No newline at end of file +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. diff --git a/templates/tools/ask-user-question.md b/packages/core/templates/tools/ask-user-question.md similarity index 99% rename from templates/tools/ask-user-question.md rename to packages/core/templates/tools/ask-user-question.md index 7e072982..bcf4ebcc 100644 --- a/templates/tools/ask-user-question.md +++ b/packages/core/templates/tools/ask-user-question.md @@ -1,12 +1,14 @@ ## AskUserQuestion Use this tool when you need to ask the user questions during execution. This allows you to: + 1. Gather user preferences or requirements 2. Clarify ambiguous instructions 3. Get decisions on implementation choices as you work 4. Offer choices to the user about what direction to take. Usage notes: + - Users will always be able to select "Other" to provide custom text input - Use multiSelect: true to allow multiple answers to be selected for a question - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label diff --git a/templates/tools/bash.md b/packages/core/templates/tools/bash.md similarity index 54% rename from templates/tools/bash.md rename to packages/core/templates/tools/bash.md index 12f52af3..c69d7cd0 100644 --- a/templates/tools/bash.md +++ b/packages/core/templates/tools/bash.md @@ -7,6 +7,7 @@ On Windows, Bash runs through Git Bash. Use POSIX commands and quote Windows pat IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead. IMPORTANT: Before reaching for generic shell pipelines, prefer purpose-built CLI tools when they make the task more accurate, safer, faster, or easier to understand: + - Use `ripgrep` (`rg`) when you need to search file contents by text or regex across the workspace; prefer it over slower tools like `grep`. - Use `jq` when you need to inspect, filter, or transform JSON output; prefer it over ad-hoc parsing with `sed`, `awk`, or Python one-liners. @@ -27,35 +28,36 @@ Before executing the command, please follow these steps: - Capture the output of the command. Usage notes: - - The command argument is required. - - The sideEffects argument is required. Declare the minimum permission scopes the command may need. - - You can use `run_in_background: true` to run a command in the background. Only use this if you need to perform a blocking task, like running a server for the upcoming test scripts. - - When using `run_in_background`, do NOT add `&` to the command. Output is written to a log file. - - Before your final response, stop background tasks that has not reported a completed state, unless the user explicitly asks to keep it running. - - To stop a background command, use the `stopCommand` returned in the tool result metadata. - - Use `sideEffects: []` only for commands that do not read, write, delete, query Git history, mutate Git history, or access the network, such as `date` or `node --version`. - - Use `*-out-cwd` when the command accesses paths outside the current workspace. For example, `cat /etc/hosts` requires `["read-out-cwd"]`. - - Use `query-git-log` for commands such as `git log`, `git show HEAD`, `git blame`, or history diffs. Use `mutate-git-log` for commands such as `git commit`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, or `git tag`. - - Use `["unknown"]` when you cannot classify the command safely. - - It is very helpful if you write a clear, concise description of what this command does. For simple commands, keep it brief (5-10 words). For complex commands (piped commands, obscure flags, or anything hard to understand at a glance), add enough context to clarify what it does. - - If the output exceeds 30000 characters, output will be truncated before being returned to you. - - Always prefer using the dedicated tools for these commands: - - Read files: Use Read (NOT cat/head/tail) - - Edit files: Use Edit (NOT sed/awk) - - Write files: Use Write (NOT echo >/cat < - pytest /foo/bar/tests - - - cd /foo/bar && pytest tests - + +- The command argument is required. +- The sideEffects argument is required. Declare the minimum permission scopes the command may need. +- You can use `run_in_background: true` to run a command in the background. Only use this if you need to perform a blocking task, like running a server for the upcoming test scripts. +- When using `run_in_background`, do NOT add `&` to the command. Output is written to a log file. +- Before your final response, stop background tasks that has not reported a completed state, unless the user explicitly asks to keep it running. +- To stop a background command, use the `stopCommand` returned in the tool result metadata. +- Use `sideEffects: []` only for commands that do not read, write, delete, query Git history, mutate Git history, or access the network, such as `date` or `node --version`. +- Use `*-out-cwd` when the command accesses paths outside the current workspace. For example, `cat /etc/hosts` requires `["read-out-cwd"]`. +- Use `query-git-log` for commands such as `git log`, `git show HEAD`, `git blame`, or history diffs. Use `mutate-git-log` for commands such as `git commit`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, or `git tag`. +- Use `["unknown"]` when you cannot classify the command safely. +- It is very helpful if you write a clear, concise description of what this command does. For simple commands, keep it brief (5-10 words). For complex commands (piped commands, obscure flags, or anything hard to understand at a glance), add enough context to clarify what it does. +- If the output exceeds 30000 characters, output will be truncated before being returned to you. +- Always prefer using the dedicated tools for these commands: + - Read files: Use Read (NOT cat/head/tail) + - Edit files: Use Edit (NOT sed/awk) + - Write files: Use Write (NOT echo >/cat < + pytest /foo/bar/tests + + + cd /foo/bar && pytest tests + ```json { @@ -95,10 +97,7 @@ Usage notes: "type": "boolean" } }, - "required": [ - "command", - "sideEffects" - ], + "required": ["command", "sideEffects"], "additionalProperties": false } ``` diff --git a/templates/tools/edit.md b/packages/core/templates/tools/edit.md similarity index 97% rename from templates/tools/edit.md rename to packages/core/templates/tools/edit.md index efa8574d..4f039127 100644 --- a/templates/tools/edit.md +++ b/packages/core/templates/tools/edit.md @@ -3,6 +3,7 @@ Performs scoped string replacements in files. Usage: + - You must use `Read` tool at least once in the conversation before editing to get the required `snippet_id`. This tool will error if you attempt an edit without reading the file. - `snippet_id` defines the search scope. Provide `file_path` only as an optional guard that the snippet belongs to the expected file. - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string. @@ -43,11 +44,7 @@ Usage: "type": "number" } }, - "required": [ - "snippet_id", - "old_string", - "new_string" - ], + "required": ["snippet_id", "old_string", "new_string"], "additionalProperties": false } ``` diff --git a/templates/tools/read.md.ejs b/packages/core/templates/tools/read.md.ejs similarity index 100% rename from templates/tools/read.md.ejs rename to packages/core/templates/tools/read.md.ejs diff --git a/templates/tools/update-plan.md b/packages/core/templates/tools/update-plan.md similarity index 97% rename from templates/tools/update-plan.md rename to packages/core/templates/tools/update-plan.md index 0c74b367..9459242e 100644 --- a/templates/tools/update-plan.md +++ b/packages/core/templates/tools/update-plan.md @@ -3,6 +3,7 @@ Updates the current task plan and progress display. Usage: + - Use this tool for non-trivial multi-step tasks when a task list helps track execution progress. - Pass the complete current task list every time. The latest call replaces the previous visible plan. - The `plan` argument is a markdown string, not an array of step objects. If the requirement is in Chinese, then use Chinese for the markdown as well. @@ -25,9 +26,7 @@ Usage: "type": "string" } }, - "required": [ - "plan" - ], + "required": ["plan"], "additionalProperties": false } ``` diff --git a/templates/tools/web-search.md b/packages/core/templates/tools/web-search.md similarity index 99% rename from templates/tools/web-search.md rename to packages/core/templates/tools/web-search.md index 92e22753..e5eabf0f 100644 --- a/templates/tools/web-search.md +++ b/packages/core/templates/tools/web-search.md @@ -19,9 +19,11 @@ JSON schema: ``` Usage: + - Do not reduce `query` to space-separated keywords. Typical use cases: + - Confirm recent SDK, framework, or API changes - Check current compatibility, deprecations, or migration notes - Look up active issue tracker discussions or recent regressions diff --git a/templates/tools/write.md b/packages/core/templates/tools/write.md similarity index 86% rename from templates/tools/write.md rename to packages/core/templates/tools/write.md index 1a969754..ce774eb3 100644 --- a/templates/tools/write.md +++ b/packages/core/templates/tools/write.md @@ -3,12 +3,13 @@ Writes a file to the local filesystem. Usage: + - This tool will overwrite the existing file if there is one at the provided path. - If this is an existing file, you MUST read the full file first. A partial read is not enough for overwriting an existing file. - `content` must be a single string. If you are writing JSON, serialize the full document to text before calling this tool. - Prefer `Edit` for updating existing files. Use `Write` for new files or intentional full-file rewrites. - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. -- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User. +- NEVER proactively create documentation files (\*.md) or README files. Only create documentation files if explicitly requested by the User. - NEVER proactively create one-off test script. Only create one-off test script files if explicitly requested by the User. - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked. @@ -26,10 +27,7 @@ Usage: "type": "string" } }, - "required": [ - "file_path", - "content" - ], + "required": ["file_path", "content"], "additionalProperties": false } ``` diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..ac00d953 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "src/tests"] +} diff --git a/packages/core/tsconfig.tsbuildinfo b/packages/core/tsconfig.tsbuildinfo new file mode 100644 index 00000000..5bef9a8e --- /dev/null +++ b/packages/core/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/typescript/lib/lib.es2025.float16.d.ts","../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","./src/common/model-capabilities.ts","./src/settings.ts","../../node_modules/gray-matter/gray-matter.d.ts","../../node_modules/@types/ejs/index.d.ts","../../node_modules/openai/internal/builtin-types.d.mts","../../node_modules/openai/internal/types.d.mts","../../node_modules/openai/internal/headers.d.mts","../../node_modules/openai/internal/shim-types.d.mts","../../node_modules/openai/core/streaming.d.mts","../../node_modules/openai/internal/request-options.d.mts","../../node_modules/openai/internal/utils/log.d.mts","../../node_modules/openai/resources/shared.d.mts","../../node_modules/openai/core/error.d.mts","../../node_modules/openai/pagination.d.mts","../../node_modules/openai/internal/parse.d.mts","../../node_modules/openai/core/api-promise.d.mts","../../node_modules/openai/core/pagination.d.mts","../../node_modules/openai/auth/types.d.mts","../../node_modules/openai/internal/uploads.d.mts","../../node_modules/openai/internal/to-file.d.mts","../../node_modules/openai/core/uploads.d.mts","../../node_modules/openai/resources/chat/chat.d.mts","../../node_modules/openai/resources/chat/index.d.mts","../../node_modules/openai/resources/admin/organization/admin-api-keys.d.mts","../../node_modules/openai/resources/admin/organization/audit-logs.d.mts","../../node_modules/openai/resources/admin/organization/certificates.d.mts","../../node_modules/openai/resources/admin/organization/data-retention.d.mts","../../node_modules/openai/resources/admin/organization/invites.d.mts","../../node_modules/openai/resources/admin/organization/roles.d.mts","../../node_modules/openai/resources/admin/organization/spend-alerts.d.mts","../../node_modules/openai/resources/admin/organization/usage.d.mts","../../node_modules/openai/resources/admin/organization/groups/roles.d.mts","../../node_modules/openai/resources/admin/organization/groups/users.d.mts","../../node_modules/openai/resources/admin/organization/groups/groups.d.mts","../../node_modules/openai/resources/admin/organization/projects/api-keys.d.mts","../../node_modules/openai/resources/admin/organization/projects/certificates.d.mts","../../node_modules/openai/resources/admin/organization/projects/data-retention.d.mts","../../node_modules/openai/resources/admin/organization/projects/hosted-tool-permissions.d.mts","../../node_modules/openai/resources/admin/organization/projects/model-permissions.d.mts","../../node_modules/openai/resources/admin/organization/projects/rate-limits.d.mts","../../node_modules/openai/resources/admin/organization/projects/roles.d.mts","../../node_modules/openai/resources/admin/organization/projects/service-accounts.d.mts","../../node_modules/openai/resources/admin/organization/projects/spend-alerts.d.mts","../../node_modules/openai/resources/admin/organization/projects/groups/roles.d.mts","../../node_modules/openai/resources/admin/organization/projects/groups/groups.d.mts","../../node_modules/openai/resources/admin/organization/users/roles.d.mts","../../node_modules/openai/resources/admin/organization/users/users.d.mts","../../node_modules/openai/resources/admin/organization/projects/users/roles.d.mts","../../node_modules/openai/resources/admin/organization/projects/users/users.d.mts","../../node_modules/openai/resources/admin/organization/projects/projects.d.mts","../../node_modules/openai/resources/admin/organization/organization.d.mts","../../node_modules/openai/resources/admin/admin.d.mts","../../node_modules/openai/resources/audio/speech.d.mts","../../node_modules/openai/resources/audio/transcriptions.d.mts","../../node_modules/openai/resources/audio/translations.d.mts","../../node_modules/openai/resources/audio/audio.d.mts","../../node_modules/openai/resources/batches.d.mts","../../node_modules/openai/resources/beta/threads/messages.d.mts","../../node_modules/openai/resources/beta/threads/runs/steps.d.mts","../../node_modules/openai/error.d.mts","../../node_modules/openai/lib/eventstream.d.mts","../../node_modules/openai/lib/assistantstream.d.mts","../../node_modules/openai/resources/beta/threads/runs/runs.d.mts","../../node_modules/openai/resources/beta/threads/threads.d.mts","../../node_modules/openai/resources/beta/assistants.d.mts","../../node_modules/openai/resources/beta/realtime/sessions.d.mts","../../node_modules/openai/resources/beta/realtime/transcription-sessions.d.mts","../../node_modules/openai/resources/beta/realtime/realtime.d.mts","../../node_modules/openai/resources/beta/chatkit/threads.d.mts","../../node_modules/openai/resources/beta/chatkit/sessions.d.mts","../../node_modules/openai/resources/beta/chatkit/chatkit.d.mts","../../node_modules/openai/resources/beta/beta.d.mts","../../node_modules/openai/resources/completions.d.mts","../../node_modules/openai/lib/parser.d.mts","../../node_modules/openai/lib/responsesparser.d.mts","../../node_modules/openai/azure.d.mts","../../node_modules/openai/bedrock.d.mts","../../node_modules/openai/index.d.mts","../../node_modules/openai/lib/responses/eventtypes.d.mts","../../node_modules/openai/lib/responses/responsestream.d.mts","../../node_modules/openai/resources/responses/input-items.d.mts","../../node_modules/openai/resources/responses/input-tokens.d.mts","../../node_modules/openai/resources/responses/responses.d.mts","../../node_modules/openai/resources/containers/files/content.d.mts","../../node_modules/openai/resources/containers/files/files.d.mts","../../node_modules/openai/resources/containers/containers.d.mts","../../node_modules/openai/resources/conversations/items.d.mts","../../node_modules/openai/resources/conversations/conversations.d.mts","../../node_modules/openai/resources/embeddings.d.mts","../../node_modules/openai/resources/graders/grader-models.d.mts","../../node_modules/openai/resources/evals/runs/output-items.d.mts","../../node_modules/openai/resources/evals/runs/runs.d.mts","../../node_modules/openai/resources/evals/evals.d.mts","../../node_modules/openai/resources/files.d.mts","../../node_modules/openai/resources/fine-tuning/methods.d.mts","../../node_modules/openai/resources/fine-tuning/alpha/graders.d.mts","../../node_modules/openai/resources/fine-tuning/alpha/alpha.d.mts","../../node_modules/openai/resources/fine-tuning/checkpoints/permissions.d.mts","../../node_modules/openai/resources/fine-tuning/checkpoints/checkpoints.d.mts","../../node_modules/openai/resources/fine-tuning/jobs/checkpoints.d.mts","../../node_modules/openai/resources/fine-tuning/jobs/jobs.d.mts","../../node_modules/openai/resources/fine-tuning/fine-tuning.d.mts","../../node_modules/openai/resources/graders/graders.d.mts","../../node_modules/openai/resources/images.d.mts","../../node_modules/openai/resources/models.d.mts","../../node_modules/openai/resources/moderations.d.mts","../../node_modules/openai/resources/realtime/calls.d.mts","../../node_modules/openai/resources/realtime/client-secrets.d.mts","../../node_modules/openai/resources/realtime/realtime.d.mts","../../node_modules/openai/resources/skills/content.d.mts","../../node_modules/openai/resources/skills/versions/content.d.mts","../../node_modules/openai/resources/skills/versions/versions.d.mts","../../node_modules/openai/resources/skills/skills.d.mts","../../node_modules/openai/resources/uploads/parts.d.mts","../../node_modules/openai/resources/uploads/uploads.d.mts","../../node_modules/openai/uploads.d.mts","../../node_modules/openai/resources/vector-stores/files.d.mts","../../node_modules/openai/resources/vector-stores/file-batches.d.mts","../../node_modules/openai/resources/vector-stores/vector-stores.d.mts","../../node_modules/openai/resources/videos.d.mts","../../node_modules/openai/resources/webhooks/webhooks.d.mts","../../node_modules/openai/resources/webhooks/index.d.mts","../../node_modules/openai/resources/webhooks.d.mts","../../node_modules/openai/resources/index.d.mts","../../node_modules/openai/client.d.mts","../../node_modules/openai/core/resource.d.mts","../../node_modules/openai/resources/chat/completions/messages.d.mts","../../node_modules/openai/lib/abstractchatcompletionrunner.d.mts","../../node_modules/openai/lib/chatcompletionstream.d.mts","../../node_modules/openai/lib/chatcompletionstreamingrunner.d.mts","../../node_modules/openai/lib/jsonschema.d.mts","../../node_modules/openai/lib/runnablefunction.d.mts","../../node_modules/openai/lib/chatcompletionrunner.d.mts","../../node_modules/openai/resources/chat/completions/completions.d.mts","../../node_modules/openai/resources/chat/completions/index.d.mts","../../node_modules/openai/resources/chat/completions.d.mts","./src/common/notify.ts","./src/common/openai-thinking.ts","./src/common/shell-utils.ts","./src/common/state.ts","./src/common/file-utils.ts","./src/prompt.ts","./src/tools/ask-user-question-handler.ts","./src/common/bash-timeout.ts","./src/common/process-tree.ts","./src/tools/bash-handler.ts","../../node_modules/zod/v4/core/json-schema.d.cts","../../node_modules/zod/v4/core/standard-schema.d.cts","../../node_modules/zod/v4/core/registries.d.cts","../../node_modules/zod/v4/core/to-json-schema.d.cts","../../node_modules/zod/v4/core/util.d.cts","../../node_modules/zod/v4/core/versions.d.cts","../../node_modules/zod/v4/core/schemas.d.cts","../../node_modules/zod/v4/core/checks.d.cts","../../node_modules/zod/v4/core/errors.d.cts","../../node_modules/zod/v4/core/core.d.cts","../../node_modules/zod/v4/core/parse.d.cts","../../node_modules/zod/v4/core/regexes.d.cts","../../node_modules/zod/v4/locales/ar.d.cts","../../node_modules/zod/v4/locales/az.d.cts","../../node_modules/zod/v4/locales/be.d.cts","../../node_modules/zod/v4/locales/bg.d.cts","../../node_modules/zod/v4/locales/ca.d.cts","../../node_modules/zod/v4/locales/cs.d.cts","../../node_modules/zod/v4/locales/da.d.cts","../../node_modules/zod/v4/locales/de.d.cts","../../node_modules/zod/v4/locales/el.d.cts","../../node_modules/zod/v4/locales/en.d.cts","../../node_modules/zod/v4/locales/eo.d.cts","../../node_modules/zod/v4/locales/es.d.cts","../../node_modules/zod/v4/locales/fa.d.cts","../../node_modules/zod/v4/locales/fi.d.cts","../../node_modules/zod/v4/locales/fr.d.cts","../../node_modules/zod/v4/locales/fr-ca.d.cts","../../node_modules/zod/v4/locales/he.d.cts","../../node_modules/zod/v4/locales/hr.d.cts","../../node_modules/zod/v4/locales/hu.d.cts","../../node_modules/zod/v4/locales/hy.d.cts","../../node_modules/zod/v4/locales/id.d.cts","../../node_modules/zod/v4/locales/is.d.cts","../../node_modules/zod/v4/locales/it.d.cts","../../node_modules/zod/v4/locales/ja.d.cts","../../node_modules/zod/v4/locales/ka.d.cts","../../node_modules/zod/v4/locales/kh.d.cts","../../node_modules/zod/v4/locales/km.d.cts","../../node_modules/zod/v4/locales/ko.d.cts","../../node_modules/zod/v4/locales/lt.d.cts","../../node_modules/zod/v4/locales/mk.d.cts","../../node_modules/zod/v4/locales/ms.d.cts","../../node_modules/zod/v4/locales/nl.d.cts","../../node_modules/zod/v4/locales/no.d.cts","../../node_modules/zod/v4/locales/ota.d.cts","../../node_modules/zod/v4/locales/ps.d.cts","../../node_modules/zod/v4/locales/pl.d.cts","../../node_modules/zod/v4/locales/pt.d.cts","../../node_modules/zod/v4/locales/ro.d.cts","../../node_modules/zod/v4/locales/ru.d.cts","../../node_modules/zod/v4/locales/sl.d.cts","../../node_modules/zod/v4/locales/sv.d.cts","../../node_modules/zod/v4/locales/ta.d.cts","../../node_modules/zod/v4/locales/th.d.cts","../../node_modules/zod/v4/locales/tr.d.cts","../../node_modules/zod/v4/locales/ua.d.cts","../../node_modules/zod/v4/locales/uk.d.cts","../../node_modules/zod/v4/locales/ur.d.cts","../../node_modules/zod/v4/locales/uz.d.cts","../../node_modules/zod/v4/locales/vi.d.cts","../../node_modules/zod/v4/locales/zh-cn.d.cts","../../node_modules/zod/v4/locales/zh-tw.d.cts","../../node_modules/zod/v4/locales/yo.d.cts","../../node_modules/zod/v4/locales/index.d.cts","../../node_modules/zod/v4/core/doc.d.cts","../../node_modules/zod/v4/core/api.d.cts","../../node_modules/zod/v4/core/json-schema-processors.d.cts","../../node_modules/zod/v4/core/json-schema-generator.d.cts","../../node_modules/zod/v4/core/index.d.cts","../../node_modules/zod/v4/classic/errors.d.cts","../../node_modules/zod/v4/classic/parse.d.cts","../../node_modules/zod/v4/classic/schemas.d.cts","../../node_modules/zod/v4/classic/checks.d.cts","../../node_modules/zod/v4/classic/compat.d.cts","../../node_modules/zod/v4/classic/from-json-schema.d.cts","../../node_modules/zod/v4/classic/iso.d.cts","../../node_modules/zod/v4/classic/coerce.d.cts","../../node_modules/zod/v4/classic/external.d.cts","../../node_modules/zod/index.d.cts","./src/common/tool-types.ts","./src/common/validate.ts","./src/tools/edit-handler.ts","./node_modules/ignore/index.d.ts","./src/tools/read-handler.ts","./src/tools/update-plan-handler.ts","./src/tools/web-search-handler.ts","./src/tools/write-handler.ts","./src/mcp/mcp-client.ts","./src/mcp/mcp-manager.ts","./src/tools/executor.ts","./src/common/error-logger.ts","./src/common/debug-logger.ts","./src/common/file-history.ts","./src/common/permissions.ts","./src/common/telemetry.ts","./src/common/openai-message-converter.ts","./src/session.ts","../../node_modules/undici/types/utility.d.ts","../../node_modules/undici/types/header.d.ts","../../node_modules/undici/types/readable.d.ts","../../node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/@types/node/globals.d.ts","../../node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/@types/node/web-globals/blob.d.ts","../../node_modules/@types/node/web-globals/console.d.ts","../../node_modules/@types/node/web-globals/crypto.d.ts","../../node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/@types/node/web-globals/encoding.d.ts","../../node_modules/@types/node/web-globals/events.d.ts","../../node_modules/undici-types/utility.d.ts","../../node_modules/undici-types/header.d.ts","../../node_modules/undici-types/readable.d.ts","../../node_modules/undici-types/fetch.d.ts","../../node_modules/undici-types/formdata.d.ts","../../node_modules/undici-types/connector.d.ts","../../node_modules/undici-types/client-stats.d.ts","../../node_modules/undici-types/client.d.ts","../../node_modules/undici-types/errors.d.ts","../../node_modules/undici-types/dispatcher.d.ts","../../node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/undici-types/global-origin.d.ts","../../node_modules/undici-types/pool-stats.d.ts","../../node_modules/undici-types/pool.d.ts","../../node_modules/undici-types/handlers.d.ts","../../node_modules/undici-types/balanced-pool.d.ts","../../node_modules/undici-types/round-robin-pool.d.ts","../../node_modules/undici-types/h2c-client.d.ts","../../node_modules/undici-types/agent.d.ts","../../node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/undici-types/mock-call-history.d.ts","../../node_modules/undici-types/mock-agent.d.ts","../../node_modules/undici-types/mock-client.d.ts","../../node_modules/undici-types/mock-pool.d.ts","../../node_modules/undici-types/snapshot-agent.d.ts","../../node_modules/undici-types/mock-errors.d.ts","../../node_modules/undici-types/proxy-agent.d.ts","../../node_modules/undici-types/socks5-proxy-agent.d.ts","../../node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/undici-types/retry-handler.d.ts","../../node_modules/undici-types/retry-agent.d.ts","../../node_modules/undici-types/api.d.ts","../../node_modules/undici-types/cache-interceptor.d.ts","../../node_modules/undici-types/interceptors.d.ts","../../node_modules/undici-types/util.d.ts","../../node_modules/undici-types/cookies.d.ts","../../node_modules/undici-types/patch.d.ts","../../node_modules/undici-types/websocket.d.ts","../../node_modules/undici-types/eventsource.d.ts","../../node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/undici-types/content-type.d.ts","../../node_modules/undici-types/cache.d.ts","../../node_modules/undici-types/index.d.ts","../../node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/@types/node/web-globals/importmeta.d.ts","../../node_modules/@types/node/web-globals/messaging.d.ts","../../node_modules/@types/node/web-globals/navigator.d.ts","../../node_modules/@types/node/web-globals/performance.d.ts","../../node_modules/@types/node/web-globals/storage.d.ts","../../node_modules/@types/node/web-globals/streams.d.ts","../../node_modules/@types/node/web-globals/timers.d.ts","../../node_modules/@types/node/web-globals/url.d.ts","../../node_modules/@types/node/assert.d.ts","../../node_modules/@types/node/assert/strict.d.ts","../../node_modules/@types/node/async_hooks.d.ts","../../node_modules/@types/node/buffer.d.ts","../../node_modules/@types/node/child_process.d.ts","../../node_modules/@types/node/cluster.d.ts","../../node_modules/@types/node/console.d.ts","../../node_modules/@types/node/constants.d.ts","../../node_modules/@types/node/crypto.d.ts","../../node_modules/@types/node/dgram.d.ts","../../node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/@types/node/dns.d.ts","../../node_modules/@types/node/dns/promises.d.ts","../../node_modules/@types/node/domain.d.ts","../../node_modules/@types/node/events.d.ts","../../node_modules/@types/node/fs.d.ts","../../node_modules/@types/node/fs/promises.d.ts","../../node_modules/@types/node/http.d.ts","../../node_modules/@types/node/http2.d.ts","../../node_modules/@types/node/https.d.ts","../../node_modules/@types/node/inspector.d.ts","../../node_modules/@types/node/inspector.generated.d.ts","../../node_modules/@types/node/inspector/promises.d.ts","../../node_modules/@types/node/module.d.ts","../../node_modules/@types/node/net.d.ts","../../node_modules/buffer/index.d.ts","../../node_modules/@types/node/os.d.ts","../../node_modules/@types/node/path.d.ts","../../node_modules/@types/node/path/posix.d.ts","../../node_modules/@types/node/path/win32.d.ts","../../node_modules/@types/node/perf_hooks.d.ts","../../node_modules/@types/node/process.d.ts","../../node_modules/@types/node/punycode.d.ts","../../node_modules/@types/node/querystring.d.ts","../../node_modules/@types/node/quic.d.ts","../../node_modules/@types/node/readline.d.ts","../../node_modules/@types/node/readline/promises.d.ts","../../node_modules/@types/node/repl.d.ts","../../node_modules/@types/node/sea.d.ts","../../node_modules/@types/node/sqlite.d.ts","../../node_modules/@types/node/stream.d.ts","../../node_modules/@types/node/stream/consumers.d.ts","../../node_modules/@types/node/stream/iter.d.ts","../../node_modules/@types/node/stream/promises.d.ts","../../node_modules/@types/node/stream/web.d.ts","../../node_modules/@types/node/string_decoder.d.ts","../../node_modules/@types/node/test.d.ts","../../node_modules/@types/node/test/reporters.d.ts","../../node_modules/@types/node/timers.d.ts","../../node_modules/@types/node/timers/promises.d.ts","../../node_modules/@types/node/tls.d.ts","../../node_modules/@types/node/trace_events.d.ts","../../node_modules/@types/node/tty.d.ts","../../node_modules/@types/node/url.d.ts","../../node_modules/@types/node/util.d.ts","../../node_modules/@types/node/util/types.d.ts","../../node_modules/@types/node/v8.d.ts","../../node_modules/@types/node/vm.d.ts","../../node_modules/@types/node/wasi.d.ts","../../node_modules/@types/node/worker_threads.d.ts","../../node_modules/@types/node/zlib.d.ts","../../node_modules/@types/node/zlib/iter.d.ts","../../node_modules/@types/node/index.d.ts","../../node_modules/undici/types/fetch.d.ts","../../node_modules/undici/types/formdata.d.ts","../../node_modules/undici/types/connector.d.ts","../../node_modules/undici/types/client-stats.d.ts","../../node_modules/undici/types/client.d.ts","../../node_modules/undici/types/errors.d.ts","../../node_modules/undici/types/dispatcher.d.ts","../../node_modules/undici/types/global-dispatcher.d.ts","../../node_modules/undici/types/global-origin.d.ts","../../node_modules/undici/types/pool-stats.d.ts","../../node_modules/undici/types/pool.d.ts","../../node_modules/undici/types/handlers.d.ts","../../node_modules/undici/types/balanced-pool.d.ts","../../node_modules/undici/types/round-robin-pool.d.ts","../../node_modules/undici/types/h2c-client.d.ts","../../node_modules/undici/types/agent.d.ts","../../node_modules/undici/types/mock-interceptor.d.ts","../../node_modules/undici/types/mock-call-history.d.ts","../../node_modules/undici/types/mock-agent.d.ts","../../node_modules/undici/types/mock-client.d.ts","../../node_modules/undici/types/mock-pool.d.ts","../../node_modules/undici/types/snapshot-agent.d.ts","../../node_modules/undici/types/mock-errors.d.ts","../../node_modules/undici/types/proxy-agent.d.ts","../../node_modules/undici/types/socks5-proxy-agent.d.ts","../../node_modules/undici/types/env-http-proxy-agent.d.ts","../../node_modules/undici/types/retry-handler.d.ts","../../node_modules/undici/types/retry-agent.d.ts","../../node_modules/undici/types/api.d.ts","../../node_modules/undici/types/cache-interceptor.d.ts","../../node_modules/undici/types/interceptors.d.ts","../../node_modules/undici/types/util.d.ts","../../node_modules/undici/types/cookies.d.ts","../../node_modules/undici/types/patch.d.ts","../../node_modules/undici/types/websocket.d.ts","../../node_modules/undici/types/eventsource.d.ts","../../node_modules/undici/types/diagnostics-channel.d.ts","../../node_modules/undici/types/content-type.d.ts","../../node_modules/undici/types/cache.d.ts","../../node_modules/undici/types/index.d.ts","../../node_modules/undici/index.d.ts","./src/common/openai-client.ts","./src/index.ts"],"fileIdsList":[[309,373,381,385,388,390,391,392,405,434],[309,370,371,373,381,385,388,390,391,392,405,434],[309,372,373,381,385,388,390,391,392,405,434],[373,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,405,414,434],[309,373,374,379,381,384,385,388,390,391,392,394,405,410,423,434],[309,373,374,375,381,384,385,388,390,391,392,405,434],[309,373,376,381,385,388,390,391,392,405,424,434],[309,373,377,378,381,385,388,390,391,392,396,405,434],[309,373,378,381,385,388,390,391,392,405,410,420,434],[309,373,379,381,384,385,388,390,391,392,394,405,434],[309,372,373,380,381,385,388,390,391,392,405,434],[309,373,381,382,385,388,390,391,392,405,434],[309,373,381,383,384,385,388,390,391,392,405,434],[309,372,373,381,384,385,388,390,391,392,405,434],[309,373,381,384,385,386,388,390,391,392,405,410,423,434],[309,373,381,384,385,386,388,390,391,392,405,410,412,414,434],[309,360,373,381,384,385,387,388,390,391,392,394,405,410,423,434],[309,373,381,384,385,387,388,390,391,392,394,405,410,420,423,434],[309,373,381,385,387,388,389,390,391,392,405,410,420,423,434],[307,308,309,310,311,312,313,314,315,316,317,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,434],[309,373,381,384,385,388,390,391,392,405,434],[309,373,381,385,388,390,392,405,434],[309,373,381,385,388,390,391,392,393,405,423,434],[309,373,381,384,385,388,390,391,392,394,405,410,434],[309,373,381,385,388,390,391,392,396,405,434],[309,373,381,385,388,390,391,392,397,405,434],[309,373,381,384,385,388,390,391,392,400,405,434],[309,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,434],[309,373,381,385,388,390,391,392,402,405,434],[309,373,381,385,388,390,391,392,403,405,434],[309,373,378,381,385,388,390,391,392,394,405,414,434],[309,373,381,384,385,388,390,391,392,405,406,434],[309,373,381,385,388,390,391,392,405,407,424,427,434],[309,373,381,384,385,388,390,391,392,405,410,413,414,434],[309,373,381,385,388,390,391,392,405,411,414,434],[309,373,381,385,388,390,391,392,405,412,434],[309,373,381,385,388,390,391,392,405,414,424,434],[309,373,381,385,388,390,391,392,405,415,434],[309,370,373,381,385,388,390,391,392,405,410,417,423,434],[309,373,381,385,388,390,391,392,405,410,416,434],[309,373,381,384,385,388,390,391,392,405,418,419,434],[309,373,381,385,388,390,391,392,405,418,419,434],[309,373,378,381,385,388,390,391,392,394,405,410,420,434],[309,373,381,385,388,390,391,392,405,421,434],[309,373,381,385,388,390,391,392,394,405,422,434],[309,373,381,385,387,388,390,391,392,403,405,423,434],[309,373,381,385,388,390,391,392,405,424,425,434],[309,373,378,381,385,388,390,391,392,405,425,434],[309,373,381,385,388,390,391,392,405,410,426,434],[309,373,381,385,388,390,391,392,393,405,427,434],[309,373,381,385,388,390,391,392,405,428,434],[309,373,376,381,385,388,390,391,392,405,434],[309,373,378,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,405,424,434],[309,360,373,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,405,423,434],[309,373,381,385,388,390,391,392,405,429,434],[309,373,381,385,388,390,391,392,400,405,434],[309,373,381,385,388,390,391,392,405,419,434],[309,360,373,381,384,385,386,388,390,391,392,400,405,410,414,423,426,427,429,434],[309,373,381,385,388,390,391,392,405,410,430,434],[309,373,381,385,388,390,391,392,405,412,431,434],[64,66,69,184,309,373,381,385,388,390,391,392,405,434],[66,69,184,309,373,381,385,388,390,391,392,405,434],[64,65,66,69,70,72,75,76,77,80,81,111,115,116,131,132,142,145,147,148,152,153,161,162,163,164,165,168,172,174,178,179,180,183,193,309,373,381,385,388,390,391,392,405,434],[65,74,184,309,373,381,385,388,390,391,392,405,434],[71,309,373,381,385,388,390,391,392,405,434],[69,74,75,184,309,373,381,385,388,390,391,392,405,434],[184,309,373,381,385,388,390,391,392,405,434],[67,184,309,373,381,385,388,390,391,392,405,434],[78,79,309,373,381,385,388,390,391,392,405,434],[72,309,373,381,385,388,390,391,392,405,434],[72,75,76,80,135,136,184,309,373,381,385,388,390,391,392,405,434],[69,73,184,309,373,381,385,388,390,391,392,405,434],[64,65,66,68,309,373,381,385,388,390,391,392,405,434],[64,309,373,381,385,388,390,391,392,405,434],[64,69,184,309,373,381,385,388,390,391,392,405,434],[69,184,309,373,381,385,388,390,391,392,405,434],[69,120,132,137,189,191,192,195,309,373,381,385,388,390,391,392,405,434],[67,69,117,118,120,122,123,124,309,373,381,385,388,390,391,392,405,434],[133,137,187,191,195,309,373,381,385,388,390,391,392,405,434],[67,69,137,187,193,195,309,373,381,385,388,390,391,392,405,434],[67,133,137,187,188,191,195,309,373,381,385,388,390,391,392,405,434],[119,309,373,381,385,388,390,391,392,405,434],[71,142,195,309,373,381,385,388,390,391,392,405,434],[142,309,373,381,385,388,390,391,392,405,434],[69,120,134,137,138,142,309,373,381,385,388,390,391,392,405,434],[133,142,195,309,373,381,385,388,390,391,392,405,434],[189,190,192,309,373,381,385,388,390,391,392,405,434],[76,309,373,381,385,388,390,391,392,405,434],[110,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,185,309,373,381,385,388,390,391,392,405,434],[69,76,185,309,373,381,385,388,390,391,392,405,434],[69,75,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,91,92,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,88,185,309,373,381,385,388,390,391,392,405,434],[83,84,85,86,87,88,89,90,93,106,109,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,103,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,94,95,96,97,98,99,100,101,102,104,108,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,88,106,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,107,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,105,185,309,373,381,385,388,390,391,392,405,434],[112,113,114,185,309,373,381,385,388,390,391,392,405,434],[68,69,75,80,113,115,185,309,373,381,385,388,390,391,392,405,434],[69,75,80,113,115,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,116,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,117,118,121,122,123,185,309,373,381,385,388,390,391,392,405,434],[123,124,127,130,185,309,373,381,385,388,390,391,392,405,434],[128,129,185,309,373,381,385,388,390,391,392,405,434],[69,75,128,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,130,185,309,373,381,385,388,390,391,392,405,434],[71,125,126,127,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,124,185,309,373,381,385,388,390,391,392,405,434],[68,69,71,75,76,117,118,121,122,123,124,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,118,185,309,373,381,385,388,390,391,392,405,434],[68,69,71,75,117,121,122,123,124,185,309,373,381,385,388,390,391,392,405,434],[71,185,193,309,373,381,385,388,390,391,392,405,434],[194,309,373,381,385,388,390,391,392,405,434],[68,69,71,75,76,132,133,185,186,187,188,189,191,192,193,309,373,381,385,388,390,391,392,405,434],[186,193,309,373,381,385,388,390,391,392,405,434],[69,76,185,193,309,373,381,385,388,390,391,392,405,434],[81,194,309,373,381,385,388,390,391,392,405,434],[68,69,75,132,185,193,309,373,381,385,388,390,391,392,405,434],[69,75,76,142,144,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,80,143,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,142,146,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,142,147,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,142,149,151,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,151,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,142,149,150,185,193,309,373,381,385,388,390,391,392,405,434],[69,75,76,80,185,309,373,381,385,388,390,391,392,405,434],[155,185,309,373,381,385,388,390,391,392,405,434],[69,75,149,185,309,373,381,385,388,390,391,392,405,434],[157,185,309,373,381,385,388,390,391,392,405,434],[154,156,158,160,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,154,159,185,309,373,381,385,388,390,391,392,405,434],[149,185,309,373,381,385,388,390,391,392,405,434],[71,142,149,185,309,373,381,385,388,390,391,392,405,434],[68,69,75,80,163,185,309,373,381,385,388,390,391,392,405,434],[71,82,111,115,116,131,132,142,145,147,148,152,153,161,162,163,164,165,168,172,174,178,179,182,309,373,381,385,388,390,391,392,405,434],[69,75,142,168,185,309,373,381,385,388,390,391,392,405,434],[69,75,142,167,168,185,309,373,381,385,388,390,391,392,405,434],[71,142,166,167,168,185,309,373,381,385,388,390,391,392,405,434],[69,76,142,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,142,185,309,373,381,385,388,390,391,392,405,434],[68,69,71,75,76,134,139,140,141,142,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,80,169,171,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,80,170,185,309,373,381,385,388,390,391,392,405,434],[69,75,80,185,309,373,381,385,388,390,391,392,405,434],[69,75,153,173,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,175,176,178,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,175,178,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,176,177,185,309,373,381,385,388,390,391,392,405,434],[181,309,373,381,385,388,390,391,392,405,434],[180,309,373,381,385,388,390,391,392,405,434],[66,185,309,373,381,385,388,390,391,392,405,434],[80,309,373,381,385,388,390,391,392,405,434],[309,324,327,330,331,373,381,385,388,390,391,392,405,423,434],[309,327,373,381,385,388,390,391,392,405,410,423,434],[309,327,331,373,381,385,388,390,391,392,405,423,434],[309,373,381,385,388,390,391,392,405,410,434],[309,321,373,381,385,388,390,391,392,405,434],[309,325,373,381,385,388,390,391,392,405,434],[309,323,324,327,373,381,385,388,390,391,392,405,423,434],[309,373,381,385,388,390,391,392,394,405,420,434],[309,373,381,385,388,390,391,392,405,432,434],[309,321,373,381,385,388,390,391,392,405,432,434],[309,323,327,373,381,385,388,390,391,392,394,405,423,434],[309,318,319,320,322,326,373,381,384,385,388,390,391,392,405,410,423,434],[309,327,336,344,373,381,385,388,390,391,392,405,434],[309,319,325,373,381,385,388,390,391,392,405,434],[309,327,354,355,373,381,385,388,390,391,392,405,434],[309,319,322,327,373,381,385,388,390,391,392,405,414,423,432,434],[309,327,373,381,385,388,390,391,392,405,434],[309,323,327,373,381,385,388,390,391,392,405,423,434],[309,318,373,381,385,388,390,391,392,405,434],[309,321,322,323,325,326,327,328,329,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,355,356,357,358,359,373,381,385,388,390,391,392,405,434],[309,327,347,350,373,381,385,388,390,391,392,405,434],[309,327,336,337,338,373,381,385,388,390,391,392,405,434],[309,325,327,337,339,373,381,385,388,390,391,392,405,434],[309,326,373,381,385,388,390,391,392,405,434],[309,319,321,327,373,381,385,388,390,391,392,405,434],[309,327,331,337,339,373,381,385,388,390,391,392,405,434],[309,331,373,381,385,388,390,391,392,405,434],[309,325,327,330,373,381,385,388,390,391,392,405,423,434],[309,319,323,327,336,373,381,385,388,390,391,392,405,434],[309,327,347,373,381,385,388,390,391,392,405,434],[309,339,373,381,385,388,390,391,392,405,434],[309,319,323,327,331,373,381,385,388,390,391,392,405,434],[309,321,327,354,373,381,385,388,390,391,392,405,414,429,432,434],[309,373,381,385,388,390,391,392,405,434,472],[309,373,381,385,388,390,391,392,405,423,434,436,439,442,443],[309,373,381,385,388,390,391,392,405,410,423,434,439],[309,373,381,385,388,390,391,392,405,423,434,439,443],[309,373,381,385,388,390,391,392,405,433,434],[309,373,381,385,388,390,391,392,405,434,437],[309,373,381,385,388,390,391,392,405,423,434,435,436,439],[309,373,381,385,388,390,391,392,405,432,433,434],[309,373,381,385,388,390,391,392,394,405,423,434,435,439],[304,305,306,309,373,381,384,385,388,390,391,392,405,410,423,434,438],[309,373,381,385,388,390,391,392,405,434,439,448,456],[305,309,373,381,385,388,390,391,392,405,434,437],[309,373,381,385,388,390,391,392,405,434,439,466,467],[305,309,373,381,385,388,390,391,392,405,414,423,432,434,439],[309,373,381,385,388,390,391,392,405,434,439],[309,373,381,385,388,390,391,392,405,423,434,435,439],[304,309,373,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,405,433,434,435,437,438,439,440,441,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,467,468,469,470,471],[309,373,381,385,388,390,391,392,405,434,439,459,462],[309,373,381,385,388,390,391,392,405,434,439,448,449,450],[309,373,381,385,388,390,391,392,405,434,437,439,449,451],[309,373,381,385,388,390,391,392,405,434,438],[305,309,373,381,385,388,390,391,392,405,433,434,439],[309,373,381,385,388,390,391,392,405,434,439,443,449,451],[309,373,381,385,388,390,391,392,405,434,443],[309,373,381,385,388,390,391,392,405,423,434,437,439,442],[305,309,373,381,385,388,390,391,392,405,434,435,439,448],[309,373,381,385,388,390,391,392,405,434,439,459],[309,373,381,385,388,390,391,392,405,434,451],[305,309,373,381,385,388,390,391,392,405,434,435,439,443],[309,373,381,385,388,390,391,392,405,414,429,432,433,434,439,466],[284,309,373,381,385,388,390,391,392,405,434],[275,309,373,381,385,388,390,391,392,405,434],[275,278,309,373,381,385,388,390,391,392,405,434],[210,270,273,275,276,277,278,279,280,281,282,283,309,373,381,385,388,390,391,392,405,434],[206,208,278,309,373,381,385,388,390,391,392,405,434],[275,276,309,373,381,385,388,390,391,392,405,434],[207,275,277,309,373,381,385,388,390,391,392,405,434],[208,210,212,213,214,215,309,373,381,385,388,390,391,392,405,434],[210,212,214,215,309,373,381,385,388,390,391,392,405,434],[210,212,214,309,373,381,385,388,390,391,392,405,434],[207,210,212,213,215,309,373,381,385,388,390,391,392,405,434],[206,208,209,210,211,212,213,214,215,216,217,270,271,272,273,274,309,373,381,385,388,390,391,392,405,434],[206,208,209,212,309,373,381,385,388,390,391,392,405,434],[208,209,212,309,373,381,385,388,390,391,392,405,434],[212,215,309,373,381,385,388,390,391,392,405,434],[206,207,209,210,211,213,214,215,309,373,381,385,388,390,391,392,405,434],[206,207,208,212,275,309,373,381,385,388,390,391,392,405,434],[212,213,214,215,309,373,381,385,388,390,391,392,405,434],[214,309,373,381,385,388,390,391,392,405,434],[218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,309,373,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,396,397,405,434],[309,373,374,378,381,385,388,390,391,392,397,405,434],[199,309,373,381,385,388,390,391,392,397,405,434],[309,373,374,381,385,388,390,391,392,405,434],[61,137,309,373,381,385,388,390,391,392,396,397,405,434,473],[60,195,303,309,373,381,385,388,390,391,392,405,434],[61,309,373,381,385,388,390,391,392,405,434],[61,199,309,373,381,385,388,390,391,392,397,405,434],[309,373,374,381,385,388,390,391,392,396,397,399,405,434],[198,200,309,373,381,385,388,390,391,392,397,405,434],[61,137,309,373,381,385,388,390,391,392,405,434],[285,286,309,373,381,385,388,390,391,392,405,434],[60,61,196,197,198,199,200,201,202,203,204,205,286,287,288,290,291,292,293,294,295,296,297,298,299,300,301,302,303,309,373,381,385,388,390,391,392,405,434,474],[204,309,373,374,381,385,388,390,391,392,397,405,434],[61,294,309,373,378,381,385,388,390,391,392,405,434],[60,62,63,198,303,309,373,374,381,385,388,390,391,392,396,397,405,423,434],[60,61,62,63,195,196,197,199,200,201,204,205,295,296,297,298,299,300,301,302,309,373,378,381,385,388,390,391,392,396,397,405,434],[60,309,373,381,385,388,390,391,392,396,397,405,434],[296,309,373,381,385,388,390,391,392,405,434],[198,203,204,296,309,373,374,378,381,385,388,390,391,392,396,397,405,434],[197,199,200,285,287,296,309,373,381,385,388,390,391,392,405,434],[202,205,286,288,290,291,292,293,295,309,373,381,385,388,390,391,392,405,434],[199,200,289,296,309,373,381,385,388,390,391,392,397,405,434],[285,287,296,309,373,381,385,388,390,391,392,405,434],[137,296,309,373,374,378,381,385,388,390,391,392,405,434],[199,200,285,287,296,309,373,381,385,388,390,391,392,405,434]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"d5c693c7241b88f974795daf08b61244f552f065ec93344f0a87bb2ea8b9c23c","signature":"9033850caf9a677b88e949247cc7bb10baf88436c87597478bb6e23eed521742"},{"version":"4743a6dba8d31255325d84ef0d5687997755332b5fe042c908bf56d8b22535cc","signature":"427af1572cfe2b3dd93c53285141b662f6ca5c5b16ee9a10b7aaeb6f523de244"},{"version":"a52c5f687d788d283ea1fa38bdc2fabe0eac863135a7dfe175ec52b309f61892","impliedFormat":1},{"version":"f2a60d253f7206372203b736144906bf135762100a2b3d1b415776ebf6575d07","impliedFormat":1},{"version":"86d4ff8ba66b5ea1df375fe6092d2b167682ccd5dd0d9b003a7d30d95a0cda32","impliedFormat":99},{"version":"652071821de280fae0ba53f0abcdd7350b1bda286f8fcd39394f5d4250de44ce","impliedFormat":99},{"version":"2b5368217b57528a60433558585186a925d9842fe64c1262adde8eac5cb8de33","impliedFormat":99},{"version":"e22273698b7aad4352f0eb3c981d510b5cf6b17fde2eeaa5c018bb065d15558f","impliedFormat":99},{"version":"0249cc57fb4f04fcc725481b5f273fe4a18d943e108724b216c762aaf311c255","impliedFormat":99},{"version":"c674b1e72c7f6879711bb0e920319b2760dacb7125fac4653702a1aa37a8b283","impliedFormat":99},{"version":"91c093343733c2c2d40bee28dc793eff3071af0cb53897651f8459ad25ad01da","impliedFormat":99},{"version":"6cc2be65d508f5404dae184fbe1bc5fc6287f2af93195feba921e619721f56a0","impliedFormat":99},{"version":"17c51065e7822de999ed5ff702aead6057c172067e485e8ffe9721bfe5010f0a","impliedFormat":99},{"version":"5c9a2aec7cf29c39450fe23930e192bb58a90b36c3f169481c3ef25f2fcf79e2","impliedFormat":99},{"version":"f4fc36916b3eac2ea0180532b46283808604e4b6ff11e5031494d05aa6661cc6","impliedFormat":99},{"version":"82e23a5d9f36ccdac5322227cd970a545b8c23179f2035388a1524f82f96d8d0","impliedFormat":99},{"version":"45160eb8c4c54610c5f43c103ff539e204d12aeed21c6cbe74c0fdf63741b5db","impliedFormat":99},{"version":"67cce3d39642e38f36602ab04d683b8a0c5e3a943eb03ac288e3d68c31596014","impliedFormat":99},{"version":"bfce32506c0d081212ff9d27ec466fa6135a695ba61d5a02738abd2442566231","impliedFormat":99},{"version":"ddaf5d3ddc45282b19fb0fecec91c87fc9b4d1f45c2ee611677345c81383c5c5","impliedFormat":99},{"version":"5668033966c8247576fc316629df131d6175d24ccf22940324c19c159671e1c1","impliedFormat":99},{"version":"7630b6a1c0ebaec2ef8e8abff850e1d6c551c47d1c345340a8ab95667460fc95","impliedFormat":99},{"version":"597b0a9ef02a28f5b1195305ec9f20a4f9948bd90ec3291d0343d1e5c0b4bd16","impliedFormat":99},{"version":"5c4081cb959a116933350a69585eff8a3eeb8c98c9b8fb0b9659a0ab51ab0be5","impliedFormat":99},{"version":"eb06d1cb283278811a37f17dc35047db62bcfa69e75f752ae170e318e4704f55","impliedFormat":99},{"version":"9bda3cb21c5022c86d2325885672085a8282a08c9df21688f7d3c6eff58efd40","impliedFormat":99},{"version":"e600e54a07ac7bcf9f0fd67722865bda454f5325ca4742e08e7c321a848fc5b0","impliedFormat":99},{"version":"ddd904d24dff387d2484b69e0643541102a0e3a4f750bb2d517f46adabf84bd7","impliedFormat":99},{"version":"045b2cdfaf5cf84f5be6bde21862add9503e104b252c6dc91efae422d4e8f975","impliedFormat":99},{"version":"19f21a767c89a4f3d8721ae698ce2a61b31d0809c1633d43c89b5aad30027b95","impliedFormat":99},{"version":"d2c24000d0dadcba27d36e6b6fed7abc2c6a9b3ed6b4a8e069be303812e86bba","impliedFormat":99},{"version":"ceaaae220b5495a8fbc15cb3925107a2b1c6f5a7ca1f4de63c439d91e7726f71","impliedFormat":99},{"version":"6496e6e04c8719315d51cb1c98452f8fcbab340a46cc859e94f3b3a5e2368ea9","impliedFormat":99},{"version":"1c521e08d75a9f4ec24e0ed84c7e7d7dfee4ab5e51aa9a4dbae76283a66d0c49","impliedFormat":99},{"version":"dbcde4d0b3a3fa5b64aaf3dc80370e76b00e8e8935a9bf1f4381a2e6299cd388","impliedFormat":99},{"version":"9242750c276f71a51c8d1ca11e4bc2df24ce1c988537c9c914fdd8ec8bb9715a","impliedFormat":99},{"version":"c4bb4f8d6dfff722a008022b3157c8e921b114d6befd8651add3028df40a00e4","impliedFormat":99},{"version":"a6d7338f7fd9035f468bc64f071edbf8ad8c6363c3bfb875c3e1c0f4f5f36b5f","impliedFormat":99},{"version":"d479a5591022bf7b46d90d10e23cbf004b2e1a30ad9b675dbd96721aafb78441","impliedFormat":99},{"version":"c7af3aec3d4a4607a9e23c63d802b77235dfe6f066f912c1347e06ce5868d919","impliedFormat":99},{"version":"d2e351dc6d967923dc6ff200bfbb20dfa92f0c23331ccf34eef1ea7d4b1d8e9b","impliedFormat":99},{"version":"0c8e28a077e369f1c26e00a3c2f76824c0ac7f55cfada1a6ff42f40b5eec0c98","impliedFormat":99},{"version":"bb8194f799c99acb6c3dc6b40866ef69cc2ce063f63666c9d1a55f6b4bb6f352","impliedFormat":99},{"version":"242cda707d18d4bd715c1916eed6ef3ab3d3c107bce17c771c803ff898dabd8c","impliedFormat":99},{"version":"7116b824716ea8c11ab831cb599e27cccd1ea083689d7085ce26954d86f392d4","impliedFormat":99},{"version":"25123c80bcb4c0f874ad6d2facf40f7cfd5e27c08a3107b18302307b7a131016","impliedFormat":99},{"version":"5fb5756d1c11073dcf4b2c76eecc0e48f0331bcb43e02ecb35a7c80f53bcb677","impliedFormat":99},{"version":"121cf1f1c42afca44d5aeedb4469d525ffeb013b78031c4a9089d64acb7a1394","impliedFormat":99},{"version":"908cb76e7cace05e1622e08912e2978c9284444ebedcdf8cd341fa75e31f007e","impliedFormat":99},{"version":"f93fe279ba4ac525dcc682b54923999538090ae9eecfeb0001b8009c70c15295","impliedFormat":99},{"version":"7e5217864cf444cb86d59472a218f6781f402c60436d823541360caa64c02244","impliedFormat":99},{"version":"37f61ebbaac9cc1bee0e3c11bf9a9b5207f8e1aa066eedb2860f108961987a49","impliedFormat":99},{"version":"263a89f026d661b338a22517a0375a19af55abb09651e73e9d7940025e2402bc","impliedFormat":99},{"version":"f9bf95954745207c3a305a59f3a8f7e36290c742d006d1ce447a41dc772ba3c3","impliedFormat":99},{"version":"732e1c24c3f5a76e61b075bfee7d2b3e5714d4960f8587b0cf989e7e151dc1ea","impliedFormat":99},{"version":"4cc5c2fb807317de6f88edae5cc2b24b705cdce764bbc1cc23aeec15d91a7a49","impliedFormat":99},{"version":"53cae4e7f0a5716f296870e5eef84af8832d5700b23ff79f349c0d1b4aa40d25","impliedFormat":99},{"version":"775e97f58cc774218eb4e979ff7f73b2fb4d958521df4707ae382b32fce5f55b","impliedFormat":99},{"version":"d93588a85b0b0eef4e6ab906fa37caa21efa1d30647aef292567c078b2e3a0a9","impliedFormat":99},{"version":"2eaf0dcaaa03f1cce8c4069c98d198b4730d6e842d393031328aefd1ed7becb1","impliedFormat":99},{"version":"d62b09cb6f1ceb87ec6c26f3789bc38f8be9fb0ce3126fd0bf89b003d0cba371","impliedFormat":99},{"version":"4a5d9348012a3e46c03888e71b0d318cda7e7db25869731375f90edad8dcea02","impliedFormat":99},{"version":"61b3add3d48dfc79324531ede7da59203059a62986070f97645a83acd3f20aa0","impliedFormat":99},{"version":"6cd8356a92fd9f1edcbfbd3b891f50228738522e79bfdad16e7fb7cfd4a66932","impliedFormat":99},{"version":"347efb60859c806ef954a67ee7520c9aa33e1881eedd40d236298af775deef50","impliedFormat":99},{"version":"fc391876e409d362cc43a7468226a9eb83440de09873b284bf09fbfb261ec259","impliedFormat":99},{"version":"d06f5012d5ac1bc25c5033f7e916fe42cc0253d6b523b9747809b71676069370","impliedFormat":99},{"version":"5d35840bd540fad886e21ddaf9b078a44c21a827dec9abc08d2d2c1a3ff27d44","impliedFormat":99},{"version":"a02182b20bcb1966fc15eac80506f617b71fdd0e279ccff44b27f2ee366b2823","impliedFormat":99},{"version":"32563899782c456f03cadc7a9508b9b6468dd678404b093bd7557d6c6e143218","impliedFormat":99},{"version":"f613a93e0685802f7f7e248156ae93ff9088d45abeff0b21b656520699b79f06","impliedFormat":99},{"version":"5471b59fcb6ad04c41f6bf57075e88f3094d9d498e51595b4341d8bfcb729bf5","impliedFormat":99},{"version":"6aeb85043e6a5d2c3768c413a01885b0fc3dbfb4b3817fde5bb93601f5efb303","impliedFormat":99},{"version":"b6ff37737d006b86082f2f7176eb0a771001e9dde9152a26ef9ea8fd80e6eba0","impliedFormat":99},{"version":"491d5f012b1de793c45e75a930f5cdef1ff0e7875968e743fa6bd5dd7d31cb3b","impliedFormat":99},{"version":"1fd56873ada3f2bf6049ae741cf4efc1c90693015a6dc3467d6c995e5a6db03a","impliedFormat":99},{"version":"8074e5e85360339cc57e7a9b6db5f49af9ef8d6d0ee3c28a02d5e0ad5c21920b","impliedFormat":99},{"version":"43db9ade57eeeb241749f460e5eec4cc4eca8e8f3c35a0542a5a746eb2bcaf86","impliedFormat":99},{"version":"53c86b81daa463deacb0046fee490b6d589438ac71311050b74dcee99afca0f6","impliedFormat":99},{"version":"70587241a4cc2e08ffc30e60c20f3eb38bd5af7e3d99640568ffe2993f933485","impliedFormat":99},{"version":"dd01943d0fe191b3b2020438367709333ff08a69d285e2f715a60711dcf83b61","impliedFormat":99},{"version":"9c7188dc07bc5ce82bbe5b75495f44a7887b0a4945f63696661fffeddca0c0e9","impliedFormat":99},{"version":"93ea079d0b9af94efc1578a95aa0299ec4054f617fb31d243f66255e221276fd","impliedFormat":99},{"version":"4ecb0eb653de7093f2eb589cea5b35fdea6e2bbd62bc3d9fafdc5702850f7714","impliedFormat":99},{"version":"69ed52603ad6430aaffbc9dec25e0d01df733aaa32ab4d57d37987aedc94c349","impliedFormat":99},{"version":"6f8acb191da449d8dbec7a4e9c317bdb6b8af104a60a101950643ea52cfa3c85","impliedFormat":99},{"version":"4c01241847f841eddf3d727aac5686d8d1e06c92124002de9d9ed2ad3c590420","impliedFormat":99},{"version":"8bba80ef1e0e9ae8c061728626309824023e85eaafcd8c285a6fa89dc6881573","impliedFormat":99},{"version":"ada6bd808581a783390b1aabc2cc836136a5d214af0d924cc57d9f29b5733ce9","impliedFormat":99},{"version":"283336202f1a6a4e13271dc83b776718cf5d4a4137b28e2d013498e3020f7170","impliedFormat":99},{"version":"54a6a3e98b7ec00fec7bd7e42ad50c16014805576ccbe33bfee04f0aac9965da","impliedFormat":99},{"version":"7c90a7108c4319b0475d5419d52f2a2c9bf499234a2a15d5b8504983e141041b","impliedFormat":99},{"version":"67fc5d1b6877a799de1e3943ed2c3669b72a6ab3b17c7b0b0387bdd6e4c1a01f","impliedFormat":99},{"version":"8ac25d431d9b1bbe3ded6c578651cc43acbdbf19c435fbbe185b827ae74ba3df","impliedFormat":99},{"version":"953ee863def1b11f321dcb17a7a91686aa582e69dd4ec370e9e33fbad2adcfd3","impliedFormat":99},{"version":"392e72d77ae33ee322d5b0b907398f2200f72d36adaca1ca62dfa7e22f744ac3","impliedFormat":99},{"version":"e452b617664fc3d2db96f64ef3addadb8c1ef275eff7946373528b1d6c86a217","impliedFormat":99},{"version":"c6a811837fef3d4ba22e7e4adcb16f12caf30252047b133404d698bf8f0e883a","impliedFormat":99},{"version":"2f722a3a421baf9a7c175d8ae6a3118dfd14c5f36474e03f99e3df5800065030","impliedFormat":99},{"version":"f9511d2a891b0a017ae31674977b053f42ca7221dedd012f6de6f75e7cb9aa3e","impliedFormat":99},{"version":"d8f262b549f3ed95402297d10b84f0f86e3113d6d570b03364d2cfca1f75e5d8","impliedFormat":99},{"version":"f216cb46ebeff3f767183626f70d18242307b2c3aab203841ae1d309277aad6b","impliedFormat":99},{"version":"d6d95f96dd5b374484fd000228288cbcfb80aa47cb74ebd3e19ea94a36e8260a","impliedFormat":99},{"version":"9abda1f0836e696725c31ea63d36a6c7c54e0f762d5e387f52b27186dff81cab","impliedFormat":99},{"version":"92fb8aa5d61dca9ab2008d49397a639dbf71c7746da23c02245523cfec4a99ef","impliedFormat":99},{"version":"9e6cd6dc690d6e6c89b17b295cabf8a5a08011ae79a7a56578a429e5ae27b8dc","impliedFormat":99},{"version":"4c7eafc682ffbd45ac24d056c63a622993048ac272c76ba1721118dc601ab629","impliedFormat":99},{"version":"f72b0af7e81183c17d799cdea2ab0d81580dfe96a98343a21d746984c3b21933","impliedFormat":99},{"version":"3841ca1577c0927f59fac8faa7cd195485c5362d99ec2b16ff9b86ec4974a3e5","impliedFormat":99},{"version":"58c5a2a520ae555e0573873a5e6303b0f1a1e70f3b376e5ac9094eaad0623d8a","impliedFormat":99},{"version":"5f8217240c95e3f3007d9968104904616287f30d853bac73874759c1dfad4017","impliedFormat":99},{"version":"7ebc96af203f866e829b528e5cffb32111a1a1ff4662bc60c3b53696e89c67f4","impliedFormat":99},{"version":"9f5ee7c037b58964c1cee63c1849fa11757f693208444be0f2d9f08defe859cd","impliedFormat":99},{"version":"33a4085365aa21a995ea4721ffff814128b126e8e346e5f064d87bfcdd0ff7ce","impliedFormat":99},{"version":"3adf214b4b307152af85b77e441d36ede388dadba2bd9962671bf933738d2a25","impliedFormat":99},{"version":"9a2cc98a7884cb530a704f6cd16a83db9aa89360a2b391a49e498b5179443dc6","impliedFormat":99},{"version":"250998ae18ea49b8745d327e7739f56464a4318783129daab90b3299bf6f8a55","impliedFormat":99},{"version":"76b3afd1f2748ff725c277bd4701f442af697c0586e1b491e6a67383a246ffad","impliedFormat":99},{"version":"4df5fc6fc2438b8e3418cb25c8c0e863d1f92e4470297d6a8756394c597af844","impliedFormat":99},{"version":"92b5f0879161f1206e30a0c219dd8f23d736f2a74a4e015885e8e3f3b3c9a3e7","impliedFormat":99},{"version":"374d12016302e312ffccd3d38e6f3df1b412378bff6e6266f3e5844af450859c","impliedFormat":99},{"version":"18d0c2293aa57e33923fc1b10970650c6d6932dbfa711a3ffd67600b3caf924b","impliedFormat":99},{"version":"17758b72f880ed66754e3ff4aeade0b82417ec546b72bf3a326cadf4e56c1915","impliedFormat":99},{"version":"ffa547cbf7599d89b6ac4c2f038f99978e0dff46bb9850df46168ce6809a5b24","impliedFormat":99},{"version":"981240d6d3015e8de441325b90c07588a302f2d9c377bccfc61e4680b726f962","impliedFormat":99},{"version":"493c39c5f9e9c050c10930448fda1be8de10a0d9b34dcd24ff17a1713c282162","impliedFormat":99},{"version":"73e4673f2da8677556210e5a127b2637bf030ab73da222ea2a19979f89d9d40a","impliedFormat":99},{"version":"e9d27f2b7d5171f512053f153cadc303d1b84d00c98e917664ba68eca9b7af6a","impliedFormat":99},{"version":"4899d2cf406cd68748c5d536b736c90339a39f996945126d8a11355eba5f56f3","impliedFormat":99},{"version":"29c4e9ce50026f15c4e58637d8668ced90f82ce7605ca2fd7b521667caa4a12c","impliedFormat":99},{"version":"8575340c8560a52c3309956add745660ad319dbd67309fa268f5af9b1c7551f5","impliedFormat":99},{"version":"3b56bc74e48ec8704af54db1f6ecfee746297ee344b12e990ba5f406431014c1","impliedFormat":99},{"version":"9e4991da8b398fa3ee9b889b272b4fe3c21e898d873916b89c641c0717caed10","impliedFormat":99},{"version":"35290a0ba8d9287d4f3635948e7e84bcf14223239ad07902d111684dfafffaf4","impliedFormat":99},{"version":"dbf3d90c21c08217509df631336881a3105740033b0592dcc47036490f95e51c","impliedFormat":99},{"version":"e6ad9376e7d088ce1dc6d3183ba5f0b3fb67ee586aa824cc8519b52f2341307a","impliedFormat":99},{"version":"2391f2f7e3819f74e51039e431d4d68dd261e40e677810316008f08e778492e8","signature":"7446741158fcd0894addb90d6f00769719782745e713ad5b201eb0132cfb6551"},{"version":"0ca51b5dc00aa2c6a3551c5b511e7ca9a1a08e12d25422bd974c2ce1707ff959","signature":"399852f50bf374c4e4c8efb4274260a0b29c3ec863e71d6a754d69818276eb59"},{"version":"ad35da447be046219c92634332ceb694b52dbc37eed13536fd18bd802ff4ed53","signature":"16d5bdedef7ac1bafc90ef5edcb5a524f7a067285e3c68b742c3def1dc8a0909"},{"version":"5be00abdcbb4485dbe416d5301421f7086cca3689dadb5ece2a53676c1d17623","signature":"9693c11eef0516a468debb1a89eaa23472061a106a7586e3465fc840909e8f03"},{"version":"91243a5cf61d442103e43100007078aaf4634840971462b7df19faa687ece79a","signature":"4e01fdfcfa9d19e0f9522f61174233fad5db2988f030045fa80a73e32dfca9fd"},{"version":"750adbd023894fa6e7af9b6eddecf7de74b5e4b537ad46f220879eb937b8244a","signature":"f24da613e5429ab6caa9687e57745be0bd9e54d5760a66f3a116f2988a79f001"},{"version":"1e693a098c4d53ddd27d499ca4d8b3ee935663aa191e62d50eeff08b26146532","signature":"99662eb2cba010fdf1202a3e222c89b8492ff68e8e206a2296f6c6ec3ee6608c"},{"version":"fe8fcf43c4614dc877d3e56d518e95d69defa07f357b2af4c385b5fb1c591929","signature":"4c0b88ca9250fa0e0fa0ce791c837487fe6b100992833f19555048cb1b47eb18"},{"version":"324df572195e93def72d9867f75dc80446d6bbaf2c9a9f0f3a45fe5b5bd8af21","signature":"dc91ed21005509f86ef528d8c3bc026e5829206e558b30ac479c6109ff0ceee4"},{"version":"f2867aef276fc96e82476403721c7cc0725df14a4e47351b93be31a28da8a7c0","signature":"a8721a1ee15a172d5d0221d47f3f1746879713898dbd7f40f3ec95fdfd379f2c"},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"ead8e39c2e11891f286b06ae2aa71f208b1802661fcdb2425cffa4f494a68854","impliedFormat":1},{"version":"40ba6c32eb732a09e4446ade5cb6ad0c147f186f9c9dc6878b90b4418ad9f6ea","impliedFormat":1},{"version":"fdd814741843f85c98281522c58f5a646590ba9019fad2efaa95987655e0611b","impliedFormat":1},{"version":"c78aff4fb58b28b8f642d5095fc7eeb79f00e652a67caa19693af1adabb833c9","impliedFormat":1},{"version":"f80a08ced8818dc99359c0acd5b3f12762e1ce53758007759b0d4e503cbf4a5e","impliedFormat":1},{"version":"37935fa7564bcc6e0bc845b766a24391098d26f7c8245d6e8ab37bc016816e94","impliedFormat":1},{"version":"68add36d9632bc096d7245d24d6b0b8ad5f125183016102a3dad4c9c2438ccb0","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"0f8a263f4c8595c8a07de52e3f3927640c44386c1aa2984de9eae50d75e613b2","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"346fffde7c32da87c2196eb7494422449dc2ca82d3b4e6bf55be1d1a33ffc2b0","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8b5875e4958528042103fdd775e106a7f76bafc29709f0690df9a7d2241d52a7","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"d8430c275b0f59417ea8e173cfb888a4477b430ec35b595bf734f3ec7a7d729f","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"aef26cf95593c8ace1c62c4724f9afac77bdfa756fb8a00613cd152117cb2f43","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"268fd6d9f2e807a39a6c5aa654b00f949feb63d3faa7dd0f9bba7dde9172159c","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},{"version":"dc9cc4abaabd5cfd8d2749c9478d0abc7055e1b1fd922dff872e6e95a4175201","signature":"2651a6c04c20c6b829234047caa3db2f1b697af6365b3387872c3f5700dc28a4"},{"version":"12262a57c1c71f8d1bc50d5508ab3c53ad4afd89fd29d97e27ebf46035b3da3e","signature":"c4d6f8fba3839bb1e5ee491b43906b9183d7b8959baebd3ec5913fef4f40b635"},{"version":"decbb658d7b354b7fdc59db5ab541e6327b20ec00c96e891769e50f77d925523","signature":"be8b6bee4b407e0540537b69d6a4874eaf930d6a8b70ecc065c796fd1437c846"},{"version":"8b61608c154f87e13c88b21b5208a3acb906ddcee5e3001f3b27ef13703b61e8","impliedFormat":1},{"version":"f6b6e3d4ec6532a05ccad69c2605b792e7473039b9f16261a199b9dcf1c33084","signature":"c3196bfd12c96e513ae6baf5c945b3bfbaa2e8695693b3628414fc732cbe9fab"},{"version":"ab30e3e6030f247c9ead0b2e6928001e1be20907637719b3559c5da617002798","signature":"8fa424847eae1e9607dad437ba29f73de7ed8a64b60f26feddf422f3e9656655"},{"version":"b39529dc70f822883c8805eba3be4b896603b8be5c21fa9a5e8456853a9cac24","signature":"11b670ed51a4a4a6847d013b9dc50601581fa09816ba6d060c8f13a94df71c3e"},{"version":"7c45a0cff6182af8c36f68d2a3523c0d74c9019b32cd0aa071e236e774b67a71","signature":"fc6cb9ca05da11c96a920ca17e25d9aef74a7fe8e523b8046c5cea481255aeec"},{"version":"e8a7565fa8cc70df160ab9ffcab5098d0cf1b3e646ed266be197f0eacc857b34","signature":"8c4244dae03efb90eb6008beb0eec636d1d5bd8576d80a7c39a4c63e389e88ee"},{"version":"4e63aa0af8005027ec3e47a329ba2bce4ad30a7866f33fa3bc9f13a702022770","signature":"ee324efc620efe75b8389fb704c27a77e6dd246ef3cb238f1151f9768de30764"},{"version":"e6c8c2edceaf3627f7816caf15dcf6393e444ba776e8902e5f65b74d451b5cc6","signature":"d7e0473f2a6a61a3ecd75e7e79efac2edb2df3c3821a437b607d0757da48bd69"},{"version":"a23023a5093a3f8a871a80c958c0948b4bb98bf0a29972758b3c902f830457a2","signature":"9ebfe9eb079314c554dc22cec3fe0071c22226c5cffebfc4308ad406883466dc"},{"version":"952c21e8d1e308d08bf902e415daba0eeb927e60321d023e94c15cb2e211d043","signature":"afdb5288a1739d52d50c117da8b6caedb777042a5fa11f02ade7b4a5bf72d224"},{"version":"6e80e675b53df355531159abae68e983264ed2364f078e27edee37df7bd051da","signature":"a6afd1c9a3668eb51fc397e00dec8dc655aa4dc52479d5dfa95afd821e9b16f6"},{"version":"e2b671779a5c5304390d08a28d3f96338e05e418c4c18624ada291261c1a4d56","signature":"b985c9126287c3b7342fd7316a8a2899cf50d3996dff2cc901000965c9f19b8f"},{"version":"71ad1cc06270386d90b3c21b1bf1f99d65cb8a31851e8e912eb68193cf60e766","signature":"a3d4dd864c61c246a59b05bce5d84c3d219567f85f9a32e539c4a2be8dd58029"},{"version":"485e4b9a91ec3ec31512ee4d7b9aaa7aaac2886bb4770c8022f9f3e20a6e1a03","signature":"17f679537f92a306af086a40d952e8a3cf2c5f99e1fd05eb6674babe31b54ed5"},{"version":"4623d7cf72b29b7f5b223b97cb8618f9c11a6013f9e3ff6519889a423994519b","signature":"bd9508d745eb868afbc50c1a262379fd19b8c8e467c21612b246b8c3b84a4d7b"},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"0ecd58f413f9bc3b7d4383eae31b0c8fc576985cd7404d6f99f8c643543ade74","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"c8b8968311ec4e5e97b7b5fb8a65efaba455db9bdcfd7fff7fb15f6e317bfba0","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"aecbf1d9e6a18dab7d92ef8a89a1444b47e1eb6134cb2bb776a26d55ff58c29a","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"1fede9296beac11ce8e6b425396a1791f64341f2be85deebb6286faf6e16306e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce697b6a251d9cad53998c7fd3098072df883b525ec45d83530e434dc6d80dc6","impliedFormat":1},{"version":"719412f054e6ecc35489462c9a21bab0323d173a7d04e55b0ace4b5d86fbeb07","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"3db996ecdee7aabecc5385976cc07eb66216034a273c07b17d1a85292e9bab0c","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"5ff4433a2deae4f85ab1377e90a7554ce6b47ae51c69a84ca30a6e22fae85834","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"c1fa52b3d014001e8662fa2669d90ea15373958a288e3b83a3b621733d25292a","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"5e66972e83eb4dc7123939bf816e6cbd9ad81af5552db1cab84e6bd9c64d2ecc","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"8413d0641f293aed551c7464615b770d34a02dedede889b9591172287d68e773","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"1b241e24f3227d078c06aeda6e050187ad59a4e591f4467abed44d92b084e08d","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"7fde0e1be5c8be204ffbf428abfcf01da2eb0f130e1bc3f539eb7275f4fd1f58","impliedFormat":1},{"version":"e284328553df5f425a5d33d36a0c3fa66b46af9d097cad6f4d2e8696dfdeb0f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"f36b3fbe2be150a9ca140da48593f21e6a8172004f92ddc549b43efec39f3e54","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"016b29bf4926b80255a108c53a1451717350059da04fcae64d1075f5e93bbb39","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"1c4f139ade4f6ebf45463505f8155173e5d7a5305e50e0aae0a5e712d6ff3b48","impliedFormat":1},{"version":"e16b319e5aca1031168de823c4946ff8e29629c4c8cc0ec0fcfe2a8ab2155043","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"2c3c5c0f54055e87640f5d233716fd889f3034fc7911d603b642369b0dbeb2a7","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"1c9e5b1a17b1fc9b3711fb36e0690421261ab2880f15b145155b5b2ba2ab6c2d","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"afa1c49f8e559e413d57343339db857d2a8159435cf9cf7d4deb41718fff1b88","impliedFormat":1},{"version":"6953d7597831d0860c7034cf4f0419687d263b6b98a4b32e37ce6d49615c36e2","impliedFormat":1},{"version":"3ac40516c33b87f751f7507346933081a26cdb8a3e11a6b3aa07d23f803c85db","impliedFormat":1},{"version":"4ac80270b6787c2b77a2d98a9714a71f4363c24b5890314f3ba582c94bfbe779","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"171b96f31e3fbdb55fe570f2a29a5ee47223fdca95a84ea2142e4cc4feaf9dfe","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"d243db6b25788f439e7e2f03c05688e92f46764351673bb0e7b2f3631232e186","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"b3fb72492a07a76f7bfa29ecadd029eea081df11512e4dfe6f930a5a9cb1fb75","impliedFormat":1},{"version":"cd6db591e858b53f23faad85346a4f43732bb33d6dff5f3f4dc47c4a448804ae","signature":"ad2664f941e8db7e19f9bae8100db326d4d1ed5701d56c4579331e575a43718e"},{"version":"328b1b71f3bedfca3331d6253b6f89aa207d876f1486a1017beebfa39fc8d418","signature":"47b10209e6f074245affea091a75da917d59c0e9bee84a78618c3a46359dc229"}],"root":[60,61,[196,205],[286,288],[290,303],474,475],"options":{"composite":true,"declaration":true,"declarationMap":true,"esModuleInterop":true,"module":99,"outDir":"./dist","rootDir":"./src","skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[63,1],[370,2],[371,2],[372,3],[309,4],[373,5],[374,6],[375,7],[307,1],[376,8],[377,9],[378,10],[379,11],[380,12],[381,13],[382,13],[383,14],[384,15],[385,16],[386,17],[310,1],[308,1],[387,18],[388,19],[389,20],[432,21],[390,22],[391,23],[392,22],[393,24],[394,25],[396,26],[397,27],[398,27],[399,27],[400,28],[401,29],[402,30],[403,31],[404,32],[405,33],[406,33],[407,34],[408,1],[409,1],[410,35],[411,36],[412,37],[413,35],[414,38],[415,39],[416,40],[417,41],[418,42],[419,43],[420,44],[421,45],[422,46],[423,47],[424,48],[425,49],[426,50],[427,51],[428,52],[311,22],[312,1],[313,53],[314,54],[315,1],[316,55],[317,1],[361,56],[362,57],[363,58],[364,58],[365,59],[366,1],[367,5],[368,60],[369,57],[429,61],[430,62],[431,63],[395,1],[62,1],[77,1],[135,64],[136,65],[184,66],[75,67],[72,68],[76,69],[185,70],[68,71],[80,72],[119,73],[137,74],[64,1],[66,1],[74,75],[69,76],[67,5],[79,77],[65,1],[78,78],[70,79],[187,80],[121,81],[192,82],[188,83],[189,84],[120,85],[190,1],[133,86],[138,87],[139,88],[134,89],[191,90],[73,91],[111,92],[83,93],[84,94],[85,93],[86,95],[93,96],[91,97],[92,93],[87,93],[110,98],[94,93],[95,94],[96,95],[104,99],[103,97],[97,95],[98,95],[109,100],[99,93],[100,97],[101,93],[102,93],[107,101],[108,102],[88,93],[89,93],[90,95],[105,101],[106,103],[115,104],[112,95],[113,105],[114,106],[116,107],[124,108],[131,109],[130,110],[129,111],[128,112],[127,113],[125,95],[126,95],[117,114],[122,115],[118,116],[123,117],[81,118],[195,119],[193,120],[194,121],[186,122],[82,123],[132,124],[145,125],[143,95],[144,126],[147,127],[146,128],[148,95],[152,129],[150,130],[151,131],[153,132],[156,133],[155,134],[158,135],[157,93],[161,136],[159,94],[160,137],[154,138],[149,139],[162,138],[163,140],[183,141],[164,93],[165,95],[166,142],[167,143],[168,144],[140,145],[141,146],[142,147],[71,1],[169,95],[172,148],[170,95],[171,149],[173,150],[174,151],[177,152],[176,153],[178,154],[179,132],[182,155],[181,156],[180,157],[175,158],[58,1],[59,1],[11,1],[10,1],[2,1],[12,1],[13,1],[14,1],[15,1],[16,1],[17,1],[18,1],[19,1],[3,1],[20,1],[21,1],[4,1],[22,1],[26,1],[23,1],[24,1],[25,1],[27,1],[28,1],[29,1],[5,1],[30,1],[31,1],[32,1],[33,1],[6,1],[37,1],[34,1],[35,1],[36,1],[38,1],[7,1],[39,1],[44,1],[45,1],[40,1],[41,1],[42,1],[43,1],[8,1],[49,1],[46,1],[47,1],[48,1],[50,1],[9,1],[51,1],[52,1],[53,1],[55,1],[54,1],[56,1],[1,1],[57,1],[336,159],[349,160],[333,161],[350,162],[359,163],[324,164],[325,165],[323,166],[358,167],[353,168],[357,169],[327,170],[346,171],[326,172],[356,173],[321,174],[322,168],[328,175],[329,1],[335,176],[332,175],[319,177],[360,178],[351,179],[339,180],[338,175],[340,181],[343,182],[337,183],[341,184],[354,167],[330,185],[331,186],[344,187],[320,162],[348,188],[347,175],[334,186],[342,189],[345,190],[352,1],[318,1],[355,191],[473,192],[448,193],[461,194],[445,195],[462,162],[471,196],[436,197],[437,198],[435,166],[470,167],[465,199],[469,200],[439,201],[458,202],[438,203],[468,204],[433,205],[434,199],[440,206],[441,1],[447,207],[444,206],[305,208],[472,209],[463,210],[451,211],[450,206],[452,212],[455,213],[449,214],[453,215],[466,167],[442,216],[443,217],[456,218],[306,162],[460,219],[459,206],[446,217],[454,220],[457,221],[464,1],[304,1],[467,222],[285,223],[279,224],[283,225],[280,225],[276,224],[284,226],[281,227],[282,225],[277,228],[278,229],[272,230],[213,231],[215,232],[271,1],[214,233],[275,234],[274,235],[273,236],[206,1],[216,231],[217,1],[208,237],[212,238],[207,1],[209,239],[210,240],[211,1],[218,241],[219,241],[220,241],[221,241],[222,241],[223,241],[224,241],[225,241],[226,241],[227,241],[228,241],[229,241],[230,241],[231,241],[233,241],[232,241],[234,241],[235,241],[236,241],[237,241],[238,241],[270,242],[239,241],[240,241],[241,241],[242,241],[243,241],[244,241],[245,241],[246,241],[247,241],[248,241],[249,241],[250,241],[251,241],[253,241],[252,241],[254,241],[255,241],[256,241],[257,241],[258,241],[259,241],[260,241],[261,241],[262,241],[263,241],[264,241],[265,241],[266,241],[269,241],[267,241],[268,241],[289,1],[203,1],[298,243],[297,243],[299,244],[200,245],[60,1],[196,246],[474,247],[302,248],[197,249],[300,250],[204,246],[198,251],[199,252],[301,1],[286,253],[287,254],[475,255],[294,256],[295,257],[201,258],[303,259],[61,260],[202,261],[205,262],[288,263],[296,264],[290,265],[291,266],[292,267],[293,268]],"latestChangedDtsFile":"./dist/tools/write-handler.d.ts","version":"6.0.3"} \ No newline at end of file diff --git a/packages/vscode-ide-companion/.vscodeignore b/packages/vscode-ide-companion/.vscodeignore new file mode 100644 index 00000000..68a0eac7 --- /dev/null +++ b/packages/vscode-ide-companion/.vscodeignore @@ -0,0 +1,8 @@ +.vscode/** +.vscodeignore +node_modules/** +src/** +tsconfig*.json +**/*.ts +!dist/** +!LICENSE diff --git a/packages/vscode-ide-companion/LICENSE b/packages/vscode-ide-companion/LICENSE new file mode 100644 index 00000000..7fd7a206 --- /dev/null +++ b/packages/vscode-ide-companion/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 lessweb + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/vscode-ide-companion/README.md b/packages/vscode-ide-companion/README.md new file mode 100644 index 00000000..ee11be0f --- /dev/null +++ b/packages/vscode-ide-companion/README.md @@ -0,0 +1,94 @@ +# Deep Code + +[Deep Code](https://marketplace.visualstudio.com/items?itemName=vegamo.deepcode-vscode) 是 Visual Studio Code 的 AI 编码助手扩展,专门为最新的 `deepseek-v4` 模型优化。 + +## 配置 + +创建 `~/.deepcode/settings.json` 文件,内容如下: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max" +} +``` + +## 主要功能 + +### **Skills** +Deep Code 支持 agent skills,允许您扩展助手的能力: + +- **User-level Skills**:从 `~/.agents/skills/` 目录中发现并激活 skills。 +- **Project-level Skills**:从 `./.agents/skills/` 目录中加载项目专属 skills,并兼容旧的 `./.deepcode/skills/` 目录。 + +### **为 DeepSeek 优化** +- 专门为 DeepSeek 模型性能调优。 +- 通过使用[上下文缓存](https://api-docs.deepseek.com/guides/kv_cache)来降低成本。 +- 原生支持[思考模式](https://api-docs.deepseek.com/guides/thinking_mode)和思考强度控制。 + +## 支持的模型 + +- `deepseek-v4-pro`(推荐使用) +- `deepseek-v4-flash` +- 任何其他 OpenAI 兼容模型 + +## 截图示例 + +![screenshot](resources/deepcode_screenshot.png) + +## Deep Code CLI + +```bash +npm install -g @vegamo/deepcode-cli +``` + +![intro1](https://raw.githubusercontent.com/lessweb/deepcode-cli/main/resources/intro1.png) + +> VSCode插件和CLI共享配置文件和数据,但运行时没有依赖。 + +- GitHub: https://github.com/lessweb/deepcode-cli + +## 常见问题 + +### 如何将 Deep Code 从左侧边栏移动到右侧边栏(Secondary Side Bar)? + +![faq1](resources/faq1.gif) + +### Deep Code是否支持理解图片? + +Deep Code支持多模态,但目前deepseek-v4不支持多模态。有些模型虽然有多模态能力,但对多轮对话请求的限制太严。目前多模态输入推荐使用火山方舟的Doubao-Seed-2.0-pro模型,适配效果最好。 + +### 怎样在任务完成后自动给Slack发消息? + +编写一个调用Slack webhook的Shell通知脚本,然后在`~/.deepcode/settings.json`中将`notify`字段设为该脚本的完整路径即可。详细步骤可参考:https://binfer.net/share/jby5xnc-so6g + +### 是否支持Coding Plan? + +支持。只要把`~/.deepcode/settings.json`的env.BASE_URL配置为OpenAI兼容的接口地址就行。以火山方舟的Coding Plan为例,`~/.deepcode/settings.json`这样配置: + +```json +{ + "env": { + "MODEL": "ark-code-latest", + "BASE_URL": "https://ark.cn-beijing.volces.com/api/coding/v3", + "API_KEY": "**************" + }, + "thinkingEnabled": true +} +``` + +## 获取帮助 +- 在 GitHub Issues 上报告错误或请求功能 (https://github.com/lessweb/deepcode/issues) + +## 支持我们 + +如果你觉得这个插件对你有帮助,请考虑通过以下方式支持我们: + +- 在 GitHub 上给我们一个 Star (https://github.com/lessweb/deepcode) +- 向我们提交反馈和建议 +- 分享给你的朋友和同事 diff --git a/packages/vscode-ide-companion/README_cn.md b/packages/vscode-ide-companion/README_cn.md new file mode 100644 index 00000000..ee11be0f --- /dev/null +++ b/packages/vscode-ide-companion/README_cn.md @@ -0,0 +1,94 @@ +# Deep Code + +[Deep Code](https://marketplace.visualstudio.com/items?itemName=vegamo.deepcode-vscode) 是 Visual Studio Code 的 AI 编码助手扩展,专门为最新的 `deepseek-v4` 模型优化。 + +## 配置 + +创建 `~/.deepcode/settings.json` 文件,内容如下: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max" +} +``` + +## 主要功能 + +### **Skills** +Deep Code 支持 agent skills,允许您扩展助手的能力: + +- **User-level Skills**:从 `~/.agents/skills/` 目录中发现并激活 skills。 +- **Project-level Skills**:从 `./.agents/skills/` 目录中加载项目专属 skills,并兼容旧的 `./.deepcode/skills/` 目录。 + +### **为 DeepSeek 优化** +- 专门为 DeepSeek 模型性能调优。 +- 通过使用[上下文缓存](https://api-docs.deepseek.com/guides/kv_cache)来降低成本。 +- 原生支持[思考模式](https://api-docs.deepseek.com/guides/thinking_mode)和思考强度控制。 + +## 支持的模型 + +- `deepseek-v4-pro`(推荐使用) +- `deepseek-v4-flash` +- 任何其他 OpenAI 兼容模型 + +## 截图示例 + +![screenshot](resources/deepcode_screenshot.png) + +## Deep Code CLI + +```bash +npm install -g @vegamo/deepcode-cli +``` + +![intro1](https://raw.githubusercontent.com/lessweb/deepcode-cli/main/resources/intro1.png) + +> VSCode插件和CLI共享配置文件和数据,但运行时没有依赖。 + +- GitHub: https://github.com/lessweb/deepcode-cli + +## 常见问题 + +### 如何将 Deep Code 从左侧边栏移动到右侧边栏(Secondary Side Bar)? + +![faq1](resources/faq1.gif) + +### Deep Code是否支持理解图片? + +Deep Code支持多模态,但目前deepseek-v4不支持多模态。有些模型虽然有多模态能力,但对多轮对话请求的限制太严。目前多模态输入推荐使用火山方舟的Doubao-Seed-2.0-pro模型,适配效果最好。 + +### 怎样在任务完成后自动给Slack发消息? + +编写一个调用Slack webhook的Shell通知脚本,然后在`~/.deepcode/settings.json`中将`notify`字段设为该脚本的完整路径即可。详细步骤可参考:https://binfer.net/share/jby5xnc-so6g + +### 是否支持Coding Plan? + +支持。只要把`~/.deepcode/settings.json`的env.BASE_URL配置为OpenAI兼容的接口地址就行。以火山方舟的Coding Plan为例,`~/.deepcode/settings.json`这样配置: + +```json +{ + "env": { + "MODEL": "ark-code-latest", + "BASE_URL": "https://ark.cn-beijing.volces.com/api/coding/v3", + "API_KEY": "**************" + }, + "thinkingEnabled": true +} +``` + +## 获取帮助 +- 在 GitHub Issues 上报告错误或请求功能 (https://github.com/lessweb/deepcode/issues) + +## 支持我们 + +如果你觉得这个插件对你有帮助,请考虑通过以下方式支持我们: + +- 在 GitHub 上给我们一个 Star (https://github.com/lessweb/deepcode) +- 向我们提交反馈和建议 +- 分享给你的朋友和同事 diff --git a/packages/vscode-ide-companion/README_en.md b/packages/vscode-ide-companion/README_en.md new file mode 100644 index 00000000..40b199bc --- /dev/null +++ b/packages/vscode-ide-companion/README_en.md @@ -0,0 +1,87 @@ +# Deep Code + +[Deep Code](https://marketplace.visualstudio.com/items?itemName=vegamo.deepcode-vscode) is an AI coding assistant extension for Visual Studio Code, specifically optimized for the latest `deepseek-v4` model. + +## Configuration + +Create `~/.deepcode/settings.json` with: + +```json +{ + "env": { + "MODEL": "deepseek-v4-pro", + "BASE_URL": "https://api.deepseek.com", + "API_KEY": "sk-..." + }, + "thinkingEnabled": true, + "reasoningEffort": "max" +} +``` + +## Key Features + +### **Skills** +Deep Code supports agent skills that allows you to extend the assistant's capabilities: + +- **User-level Skills**: discovered and activated from `~/.agents/skills/`. +- **Project-level Skills**: loaded from `./.agents/skills/` for project-specific workflows, with legacy `./.deepcode/skills/` compatibility. + +### **Optimized for DeepSeek** +- Specifically tuned for DeepSeek model performance. +- Reduce costs by using [Context Caching](https://api-docs.deepseek.com/guides/kv_cache). +- Natively supports [Thinking Mode](https://api-docs.deepseek.com/guides/thinking_mode) and Thinking Effort Control. + +## Supported Models + +- `deepseek-v4-pro` (Recommended) +- `deepseek-v4-flash` +- `deepseek-chat` +- Any other OpenAI-compatible model + +## Screenshot + +![screenshot](resources/deepcode_screenshot.png) + +## Deep Code CLI + +```bash +npm install -g @vegamo/deepcode-cli +``` + +![intro1](https://raw.githubusercontent.com/lessweb/deepcode-cli/main/resources/intro1.png) + +> The VSCode plugin and CLI share configuration and data, but they have no dependencies at runtime. + +- GitHub: https://github.com/lessweb/deepcode-cli + +## FAQ + +### How can I move Deep Code from the left sidebar to the right (Secondary Side Bar) in VS Code? + +![faq1](resources/faq1.gif) + +### Does Deep Code support understanding images? + +Deep Code supports multimodal, but `deepseek-v4` does not support multimodal yet. Some models have multimodal capabilities but impose strict limits on multi-turn dialogue requests. For multimodal input, we recommend using the Volcano Ark `Doubao-Seed-2.0-pro` model, which has the best integration. + +### How to automatically send a Slack message after a task completes? + +Write a shell notification script that calls a Slack webhook, then set the `notify` field in `~/.deepcode/settings.json` to the full path of the script. For detailed steps, refer to: https://binfer.net/share/jby5xnc-so6g + +### Does it support Coding Plan? + +Yes. Just set `env.BASE_URL` in `~/.deepcode/settings.json` to an OpenAI-compatible API endpoint. Take Volcano Ark's Coding Plan as an example, configure `~/.deepcode/settings.json` as follows: + +```json +{ + "env": { + "MODEL": "ark-code-latest", + "BASE_URL": "https://ark.cn-beijing.volces.com/api/coding/v3", + "API_KEY": "**************" + }, + "thinkingEnabled": true +} +``` + +## Getting Help +- Report bugs or request features on GitHub Issues (https://github.com/lessweb/deepcode/issues) diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json new file mode 100644 index 00000000..78ff140e --- /dev/null +++ b/packages/vscode-ide-companion/package.json @@ -0,0 +1,85 @@ +{ + "name": "deepcode-vscode", + "version": "0.1.22", + "publisher": "vegamo", + "displayName": "Deep Code", + "description": "Deep Code VSCode companion — AI-assisted development in your editor", + "license": "MIT", + "type": "commonjs", + "main": "./out/extension.js", + "repository": { + "type": "git", + "url": "git+https://github.com/lessweb/deepcode-cli.git" + }, + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Other" + ], + "icon": "resources/deepcoding_icon.png", + "activationEvents": [], + "files": [ + "out/extension.js", + "resources/**", + "README.md", + "README_cn.md", + "README_en.md", + "LICENSE" + ], + "contributes": { + "commands": [ + { + "command": "deepcode.openView", + "title": "Open Deep Code", + "icon": { + "light": "resources/deepcoding_icon.svg", + "dark": "resources/deepcoding_icon.svg" + } + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "deepcode", + "title": "Deep Code", + "icon": "resources/deepcoding_icon.png" + } + ] + }, + "views": { + "deepcode": [ + { + "id": "deepcode.chatView", + "name": "Deep Code", + "icon": "resources/deepcoding_icon.png", + "type": "webview" + } + ] + }, + "menus": { + "editor/title": [ + { + "command": "deepcode.openView", + "group": "navigation@100" + } + ] + } + }, + "scripts": { + "typecheck": "tsc -p ./ --noEmit", + "build": "node ../../scripts/esbuild-vscode.config.js", + "prepublishOnly": "npm run build", + "package": "vsce package --no-dependencies", + "test": "node src/tests/run-tests.mjs" + }, + "dependencies": { + "@vegamo/deepcode-core": "file:../core", + "markdown-it": "^14.2.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.1", + "@types/vscode": "^1.85.0", + "@vscode/vsce": "^3.6.0" + } +} diff --git a/packages/vscode-ide-companion/resources/deepcode_screenshot.png b/packages/vscode-ide-companion/resources/deepcode_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..3e1f2a9d4cfbb0390034474d1daddd1897cf6568 GIT binary patch literal 357519 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 z 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%0D24v6 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ÄutX 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%0AY4-%#+&uKg;9Rbbb=< zCCCLN5o!#jVn@EC%{l0CGNk&=!O003)G~y#&I5BzT@+Kt4{6SxXJz z(yH$NIbkD2(<2xaKHB)NW6HNH(?h_SnDk?iS}>4zF}t$C77Bb!p z?Dy>W(i?VQX9^1b09u&xAqpN24xl$N5PBY)Pn$huX;~6wKs>UPC6yz*-40>UyjioO7oW$;0uW80Y9-18Uo|FfWdM$hgn;mE7lI=i)!L#qtY2d3wlqY{ z%w^0ie{XSr!vPC3D7d?q!BO%S912y_0Q7qSZkRz~0Hdj+1N2bN<9Z0{MlIOi z34uw&3510Z2*T+HfLF`Oyx~;dAY=>`0dPzk%R~eGO4*U-)TYgV2MVDcEZufR;cCx= zW|@=nBbuIkqFuA~U{EilzcW@z7_wF=ftca?QTL;2sMjWI>|V$SnfoDzPFlyuq{9sTY2dM9g? zpsF^{`pQmmiM#q2!J$=N!1KOv{tukza{8Ns?)T?#RdMt+U%bkRQ8)QI6~C^hd2fKxQLxg@i47Znhzh@ z4fc|%$k0YTyr;yRO?)Ams&P=9m${;5Y-r-t$%!%7nOS`Qb9B#?xyALqZP+<^Uq$PA zy9c=^N!622`o|hiEb|AYSI;fnai9FeGd0!wj)Gw4G5))x-x~y-qrA%rViE4mve>5> zOx21i@ncbOF6WRrhW1)v?Z0qrdVj8&xw{d>(W*;(%hv~szW3W3|KF0zD6=hsnj}By zxa+(ZqI3{n65DS~#A(bpqHCw67ZpPJron~Wi>j8(g1zvqpkM0v{aTbc^x z%Xz*_|Cw+Os@(U23Buf064$Z{`iV7F{2{Abd5_&2OD#3^R(=^c*qk7ime()R1g3IB zAMr7%kuBU93U*j9J?i~2bEr|^m-0c}U3&Ua^ZD`jW~*ZfmYIH|g(AKDl%=g)oW{?R zt9|pDw<<&p4`rg=EYleu^F2=6NBZ6y8}>@|u3RF|7x#J{7fVVd-diby-rnvU9(yQ8Ex;%A9I60>ou^?E$`3eT=jNR&L_Lz+e zNU}JdkC*NNp)%_`4$wNOvF}xezE>)^gmWZD>M6JDi>H^tdjAT0PoMrHVUytTtvGOF z8cB{fW>S7{aDGsN?G2Ezhf|6!zZV&!d%*_(tSmpRXw&H&z|E~d$%WtsxY%&>*I{iJ z<79J!YdlNk=mz8E_E#i>>&r7}0_;K4YNJx}M@NUz)mC|@(mKz`6{i_rtg zR~FGV2$=qzQ~*N<5gL4tbl(7E0O*!c7HW3Td15Ik@Ha=8>4cOQ1F^DH zEAtK!`F0uF6@=s77Zk&J-B?XRq7H=PA>xVS3; zTgZ&%SI!e+%tRuedx*bX@(*$OwxrWC!>`MVsViTy$Z}-~1UFoD8x0;6IsU%o+aFmF z0NYl%I_5tPUYqML%xY*eU_H&8PiQ??ByH6&Ow2)vL56Dm_@W zMau!`iLLEl`&z=I8{;FY|K(nKD)+N{X86Z`d@2vS-S+j}cdrUl&nTTzl~}bGCzTxH z7if)?i7b}CW$lK{SZC%tI*0Hz-TMf_#f|3R7CG4drP3;cq4`Vdg$xmG41Kl7Lg!Edaf-QF`Bg}ORQ1?* z0~JL68a%8kLw$1ymOpkiTZ^&z;z}j9c%6Sy2<9VgN!fjsPhDF3?RS{tBBrh%1qWt$ zCA;yLH?v|KkE2u)l@~Xbvs-$DpQ4%|*RilttV>G{6-W@J4cOB#hwF1J@E@+LAil(R zPSd*$U^r6{_M43fChRo0Nl+^p1v?(^jxQ0`D0htA!^>n7kju?@H}!^qvuyqDG@zP<3?RmZ>&~S3sZw~AJ#g1u3@~)8=Y$18>f4KttRsZi?g?W zO-A@O!uOhV4WjT9qd=8X>{r-dMLHI2kmfY_Mq~3IM!)oV{ps0bzMfzG4N0tCHj&eE z$DLc#ok_AO2b)%B;huS8bvfsLDC>w9j+IBs%-YN40#noDTLH4D85%DiPL%E`KSV(Q zj4uk`v&|O?6lq{Afdt}l<_E-?IS2!%GC5O0XJy($9uNsF5%2}CsJzsW1Tkjo&GlQv zOoOwv1Rvw^5~NMe&LxYzXtkXmx|SfV$`m>RSq>OwY`+@P`~__T2}yEuB(u&Jc=X@K zo=+9eg2+Jy@WAj(MP+2uDMyI_qM65!$BNX=*J3giz=p)|2J~%mcgMSmR0_c;^P|zV zth`)-IvUQ2yC^6v=UYWufz@EN0N(|$;lamn_s*Sgd{!d>)92<+csc{x_3h=~ipItZ zXx@OxG%Y3uc%8Ry-!>minry6#OYsA#D3}+SXldbwCr0n??ly1dEVR@B)v8f#p$G_1aQ|jyEnAlrWMTK1RDxPV zntFVRiwkh^<>k0%B(O*z|C#Rc4Z|cAOvuoXUmJ|)b4gcY6E`;Xqm;s@PGaO-3o|i!2g4Y3O@1( zGQq_;Eor$LAkc!35dh8t9>+aXQ=DL?nN-)eWt3C*_P&Pi^(~3 z$W&lhpmYwa(}lz@lOfxM>x5*Tm51%DO{*aPbaeDJ^xTO(KIZp&-9yM=?Y4M#e7HRS zV1p=k*f#Im4Z#1RgsD%lf`ph3N)m%rKBEWbui@_=uz&Hi9&XWk&ck(#_R=p%hKl9O z{(E|7YLf`%P(uh97(A zP$465tK6OQ3|*=2KI2@~{MNaconCk!QMjE$g3Xswq8W3!Vt4wj_v{3rpffHC7)^)C zxIc@{`OGV%F4U94n#~%cRI2!K%GsG}B~Ke2!s&vVc4ZY!AHGLvGaAWpFukMi@ymPi zD>)Ne@onGa)QV)16TFjMMlEn1!+HzAb|!u6RV#-dclO}&R>%2HxV zP4;uBKC$aA!Z0f(5-Au7cV*VdS2K}SZTPr&MNqKjHX;|jLd>uGq*_aOKmT!0VE`NU zaJa+U(V`p=MU}|_9Z%1cf-Kgyt{o38k_M*?uLCuDQeK+36(w0)QCUkNotvn*xZanM z9i8hgJbhwbmoTG)^`kS0w?s3^6BD1CUxWU1#qysd0={)nP!Lr7>lTcle;{buhfT3} z0}crAiuu_T!*TqSNnKYr)l7r?EBpj0OuoIv=0?zS^;v>l*HZo8fcaeW+ZSW_Ha67^ zhX0zCqu!6^Yi*N3sc3D~2|iVY8CK|Y0LJ*a(}sePJV<~J)ssP%*HzdErh@!@S_JtG zMPe<@t&iQI!NEd@R7*f}7O#k9AN6wEo?*gvl%d5gg+>KD6CiQ5SO+B?q_Mz7rt3O% zYl8D7=DxoHh@3#BlSFC^Y7e~{D;(2xcn^zi%V>Q=a=)2ngH?DMcxNBI%l?N!u8QC8 z0l7n@{5MbpjeG{;wk|S8HYP>8e`VV6I>-c<0m={svN|nxn)f+fg%3P{;0pR)Z5fRum>L;b4(=YX6=Ah{!vP4Na^@I1nZV^rFP4)d zd12wjWru+ijBD{HXBOrf*U^uK5)}qEh`UgiIg33)VPpGXZn!L~)v~Ca;Np(2iJED3 zBG_<~EvECMotS30=!cv#-Ri$G{*Vb}R#QpYDQ%|v8>_4DG%|Tqmb(=3g@cb6l&z?> zYXigA@pq-wkA+oKQ{FD$W9F@Bsm2m6h*YGXuT%1%~oUl`S*JeDDcs*t=?($+J8Q2tfok+B&`dB|8 zy)ASyE{aHhKHG_VJz)&n9=h4;$C9>9hjx*c(vtJnZ>Q0CM7s>rEshV}3 zAI>@cZdFOXj1H=HJ1Tx1bK6(xPQR;}VxPq911h%NTr#JxCzOahUTpeE{>JqLyd&Wg4_#{IYi{DD0Wahr) zmo3ek;w}LR^o=$chG@zB!V<1SNxZXu2QXHl0F7|xiTf5H1S zx+q(Pe%7WIhFhwtszO6U;rFU4M<{Ipq+d@(<#_*>h%S?#;UhZSTes2ls{&KhBUxB1 zM$#c3aIF6h8bToKKY5Z}T^+*`2DR`fjx0f)nl)Cjd3om09E5p=grp=waBoFMrW4eX z2DbWam(=m`^MiTWzjXc6Q|XS*&ijq05GF;=fw7wnWK2NqkBNzap#~VS0Hq$v-Me*D zLKNn{YRmy(uCR_UETJd7Rl@G`PvrX$AEsR1GyR74govp-B+ONIB z8IN*0M+4!zWlo-hO?!kd2^^#v!|+3>WfJUfuDyhv(^)|2Wx@}gflYW0fa%@)Y!#36 zBlH~0L4AaVpN{^C8%zRDybW##6tle|Oz|LMMh+p+T40z}u)PF3{`K?*pQVcn=b_*Rw18pM zn^3T<`7{aSS(;v#FfwwlW;rKb4_d+&8sLLT8#IeH7gayT3#_bUTQRA!|0TS@u?ep* zBRoQsL3Hlu@4+&`!*4K-!vuAP-8l}DtDDFbI$UKmN}~!l==UJ(m%iSQd13u6j6nXg zQ)OTt$1x#FFt{bT`S?76Sq?$BMYio?6Z|pdo^Js&ca3SgManWz7IV{{Uw)!_!8s(p>+#YezfUBj-C4A|e{V+6 zX`hHl>H6vw6E4D6i5fG;{C;1R6q%m4lg(9mV6yJ}rtC9Ds1?2zum6=Nf-% z>@%0g&2880Pjh${Tbn(Hbfpg|wFZoxV@Owa72D$%;ZXLfF}zs7QpoS!=`cX|`ff!yfR`bEC;V^~*749tQ1z#DtK)2P zB_2)1U%WPYiVzhn{QSHR)BSC_2s2u?xU(BNV~KT47Q+S%r=lbelJdIP$jA)9=hfNK z(Gy1tdT(1cmBLf5IJlX!>%XoS5kKsI|PJ<40P{Z5rCz)@a1?)RdI|!FnA3&V`p*qdjXjP{-+l$1FWPJ>qAq zx?aYY9#K(IkXm$4O~v)E&dtn>Y#y(ytRy8RrKgi;hk`N}WUviiz8vq))KKPtT?)226OAU;kYOh1)`dvk%DQ;pzbs7|3u#oHyc`3dYw!6^7L@2Ddus zYaSxN7NdnW3q^TG`m`6XKTkb*N#*-wrqPvo36XSgsDU7WU6`w}9+lA7|Djr> zc0aHK?&!yQ)G!1AS(ZfBt(;gAtp4P9NA5JgM-pbh1!d5kJD-xEYg}&F z8TsNpOr?)8Cdin<^D;Ygz z2;@nO)~)hf9=AsKN*EGf6V05%7{|?9#Lt<6=?A>X5h<6(#LUkXzdmEH`{P8&712Is zB(a?HLrIbl(Plbgg3>Q~6o41`MSqR5rgP|Zz;r_W8v52#vslXmOLIt|I~8} zdBg4#lKgt*KmB`{k2iUiRzJOsNA8;K`0)MwkQh5hDktX0Li5;=px*PP=#bc|aC097 zt(O}CLTWGV&qoazIo3lXBO&RxOB+`h-=l37Zh9%)Au z%+;I!nf$bKh{yl#6#8utG(~Cgn#WM;Q!$rCXTHVmnm;+Ko*0~RD~~_Dm*U^_FpMW0 zM!w;i{}2*l2~u{r%;7!^zJmJ7<9o;($R+y6M@L!Xo4;EwN`oXEzp8L1U%_Z8n9E#Q z9AdJvvdrWHIn$BtCAdjJkSQ$pi~Mm&Fiv|C6%|!XY^89Pe*G!q3A zr@XF?rg8KkA-g04ZjlOoi35pL*4TSIKWZ2#pnc(BZ@&+BcJ(Pbx4VOh36<7M`KiZX z=l(3XJBDhDU06%R#ZsMl(&I(z6KoXSK{OhY%M3J(s3xy=0V z;b?f~V*6SjEOlw+-PhV81+S%^WsBVckL>Mp2+ATDI zllbdb(FzTjf$yao%&1_^!z(PDI_3+GiNT&=%27&;kCzBY9^XQ zu<)%pd=4w@L;aViqEIDkXRW*k1GXU(F<|9+!<8~6jHg7+)!yWO^biY7x+us{!J@Xa zfF#rT%^Ro+zLJ-J@pBpzCR}NG$WYPWf3<|uWKXiDW=l-cWAOhy0F-+fUyHVj?{EzW z##R57&M%GkEB&0io25_~wlOI0fXW#4tr6E)`t6_52`7zjxV3hjHGCiv4(IpdZ0gL< zphtWE7ej&0DSRf2x8nof3bMf_F_)R@gTU61vMxQDEdB>WlzD361vW(<^f{lzwMx>J zEHv$^F1&cxh40;i1F}KH+@%lv0tr zzy6yi2?|cu{AGkE(LZ*zCwVt&D2QP@1K{`x#p{t`aH;BE!1W(&kcxt!nn^$N+ zC=BTWjk~$3Dh{_3*By!i6apU~A1Za2*SDJ~&p->sA&7Z03tA-@5D)mJs^6fp`zW+a z@{{FY{Yz%E!K9iu%irKu=jGvf9}qzP0~^pqaXgQDrq+J{mcMTf5-EGo+Bx96`av^1 zN+R9gU&O$`0PZwrXO4>J6ckJF!q*SnLU9>A;6{ezWMN@J7M3*%-q5A1PQo)f>B(ny zI!4A+$i#SVCJL0bq)A~~k^5IwaxzgN{bO`Y8EULmqIjtsxTr;0i0gH(l&GU5Y5nfz zXt=t%4jMrvKPn^9X4tes11e199WMMY6z(K|Lr}3%yP$FJFM6*;cWO_Rd`slj>cab<8U6cHMY{K- zCOCvy`iF;UTwn!-Y=MsshZm~XVB_T$Ai{h$JUsl-llp*_kr9B%qheQL|0>woi!~&Y zAG((_hA8acyqSgCzZxC9Sn2V*p<^0Ua#_()w@9$D15ST5xmQv8-BlvEf8qf#NesIj zcI0>AZxa&}EHxr!hC=&e(u^H)a6H@1R>D#Rks8v%sI|8ti^svOrjJGsmke7x62J8z zwr)FV%F?Xdsqu^(sLE6u3&jq~&Tq)7kob(Dr0C^7=Zr9L+^KKzcfQF?&@YI2HMmnv zWB9zb8YTwlbDCQG8W`$WehpkT7a^q&SAV>f+cK?nCgooAi^0laVwgCIHA0j_Ysn@- ztLUZ`b~pPnQ1qT4@Ex^^lR4ej#K}vG$Z6VfRV_n2Y<^|Rjj8HqNbo&dV18?BS13hi zoTRD2^}_tNaq>K}j5kIs`3g?$fpcs8B9j!Cu71>lLhPRvC6%_AoCb9zJQez<88kK0 zL|#@J(rBd3si8lr6eREPZ58XkWmz|T_Xy#g^n1R0;gb7%)WfVwJ*in`#k@shpvU;J-i&aWT1z zAh%6mHEy77zkr_fXqmOHW-+tmzEH1ONNRg~JM=g-K~g;15@M7N$$~3n45?<^T2-|c zdYw@9E6C0cAkLv@XUCq^gV9vJq1db;aD8Pc@`ML>#q z9oX+OLw+7$CdposXaG6YqWd}t#Y6SR4Go5_L=?mFMKc!qSk$ik`{?L)yvVrR{Jctk zLEKD>;zV`tDK+&L8|HgZAFJi2K~L~LDOlF?+aUF~xRPqbdN;yF=P&wvHVg^{WiC%> zbzgZ#6+cU#vqHZOT`Z|UBBIG{ZrZii!nk7t(xaPKVnP-UWlh4mTT<^kf06$#h1%!J z;v2~EE1#n!!fcUnJRdQfEGP(yeZuHoQA@s3z2@`wyJQ=S$R%!X- zr*!^`#Hw&2UI;>}M_>7v^kbLezxpdjegDF`q&0^p<&GJ|HeBF;ruEkHqm-r9g0zaVhWcO0CzjdSXc7s0{rbA+8-%ld zWhy<&5w;_IKfY^Tp~e%ravAg~@hm5XP_3eTcyF>hN)dzBJ$u&{b~slA3(i^v&&WBX z7!V@_GSYd7m+orUC0Sm%%;rh?H#aI$tKnqp2cb~!{QAC#nv+rZ*_QV#*0t6mRX)Pk zGC5kqZ1AItkq!{rm4E=`HtryWqs6v8m0|*azo@PvP4D#J`7BDf(rN=`J*^1JQzuomf_t{)5~u-sUZ{w7r7&q)AX(TH ztN4Pa{H%}o1G5B?NOKKei1@?m1(yPRM6Evk%i8NVSJhud$d+v1aOB`3ph~LGR^UGH z)qLX0vk-wmao6(Xl@G;~#e;(C-DkDsnJ|wjz+|)W$EFST{+I8^zE|~an-lgjXDW)0 zyo*3wI_F@qiMw$Z{t&n0D-9>!6xtC??AiYE91+FNyZ1&nQsPKkK{H&mXuQ*2P&)hh zZlmJ$xc1=V-!&37lDp;0EF7(3mr^eE`B@xO27LzF5c%+zitwbx`-dco3%hw~OBtN* zgu<$cp;t}WM>DpKT4J*r1()>@AzHOZqt)0_lb?18Vm;{)mo@AoiA9_*@W1CN)jPDm zwv?<(7MSopkWT&08+bN+1iMyi>+7&mXtU5T+0`ZXU%qynXKMIA&C`u5-`pZupyr8y z(EB~w;P50ABATF*3XS=w3QK6@fD{^lT8+P3vNX15Y+xj(nT+4#7__-_v$L3f5-u*a zY6U2pw2({*my#kAqpw5zMtJ_85VW8m!}H(H80{ivrq9haC#UYGO7t%Ky1F`y2M@O0 zEFoxSq^CcJ{&)l_zf?XKq$=buaqow`KrYI$!l7$N`e)1x~j?}z&P=Np{2p%#4a zfsKpHX|8NCIx*KibS7jQZ=gv$DsHWJ51BsJ3Jxxd&2z}WwWNj9DWNKmB_1yPP6rH? zA+ZaPCN$(BPm{!Q-mEb_rK2mTa_Ed4ZT+K`%Bc>RpN?7{@Q5AB?Zl+t9$n zz8OKxEzH#iEpBr2dnH4(=VM8vuruC32$Mi6=lt60+H{fLj#SKiJ>TX>sT!N!3{Q^9 z-FJI5*S)q;B8A{e{Q6dDj_VEibo&SAUwW$D@l=9j4zG!YiIUp=w6b0)1OoCORw1yl z&9xhpNJx>|+}pTfUR=z*C%7^ztv%S#*N)UmaMMx&CAAav{FH0()FH_&iPZ%LWZ!;$ zF}E;}K-1#AmxTVS&#%#-PpsZZd>Bef^ILbD&89N2sxE!Sa;`O5MT$`X`C(j$@{c#G zmeV#a@)6BS#T03yWy_*P7gJ>)H z5$Ege*!Uw;CmCH-1cdMK>;BuVQ!k1n~vtmWDB_25nxUZ&I3BP)ok9lSi{I zK+39nmOgGldP?mcyCh!l@1}Y1jb~oF!p?O6wRxfXtG9E>ECgHIvy-k={%42wL-WgT z&$ts8y|l>C2+yCZq)ZK0Dfd)6A6{7UX_nzIUjQ!UW9* zL6qi=WurKy33BE9#n~jz7C~1PUscUicKQa5?Oqs)zJGJS602=7f$hAijZ1AUOtbwI z(YDGmIO~MnV71$xQtr*!{5Emz@auu4b3kky%N4Ya5Raz1*I*71acN&^pwv>H#w$Us zo+?+!kZ*Mh;mD~w{`xlJ+iBJHkwb+N?3YX)j;*w7A%A?gh%4y@lde)Dv}t@_<#H9M z1xo~7TT#OkGg{u4ZI9N;i(F`Bt!!CitRi1%%Was8yvP*O9bLN0WzJ=wjHrkzf0d!q z-nSs1i&jIlWZaI`6Y(uRFP`10$y5F=mNG8vdB7{sIy4t|dsO$a=*<87d1vYqb`*Dq zCAKZZPc17n)ITPs-y}V>e8@|6w$I?ymRyu(w$!M2W<4Z%moVIgG3WH99zTzv3>hOQ zFEq`*Wy1bPi{y2?exiRfV~EJFmz)7UN*;zHW8)r63La7CTe@#ml9i{5Nrs4)4P+ef zm$X<&N;GghE!7gWWE0sH`Yk5va*&aj%wCLbBMkcMJ64F_)zoX9*VLG_@KtlWui5MO zKfowlm72`7wG8dhZ6*#&i{a4o)(HPxFreW?V|S#&_C>xdXX3G*6yB#I-1ofEBA<*{ z8VpQKs;wpqVfzCV_t-c%8L_`%28zcswhv6abkp|lO2GF$Y6eSWl2KSZ&s>z1l*XFh z{t*@txj3;F3{>^-fO5t(-I-yn=1UoXxoc=!H8(f+x(jS= zZ*zEw-ur6AY8uPVjS|(`DmaUgo1QLIprWOr5xIa#D_zJN`svf-6}@t&&9MmW<3~_^ zfKD>}f|@We)i*acA;WTn))fSEP(k(g*Dijl1@>upt+RBk(gtQ`wUBXgSWg9uW zEYNZ?rTz8f1~O0HPcnDJ+LsR803%~kR59gtrHfjjs5OIaPJ+BK1aSd#|V**<#`22Md$he*f|>Uq-pR zN?GZ9FR%lqOO+x7-1ZTxdv>=ARL=RXp1^X<9?ae3b<`Nh5!tvh)uSTzR#ERYK1L-~ zV%)KKfByyV@`#Z2@pYQsn9z)AW{7 z7OX|=xb?Lk*AjHmYPB33HB1XA%tb4L&cKaM|zES2A6{+{qgy=;0cwE#G{; zn|17{jYuc|<@Qq8@dM+EO@`*$&pSj+-k7;DuaH+Y>uOs>1XfrbB1C>l9+E!iRZpvU z{ckYy>6N&9t0J{MYg*E0OoWHLS8EGJ*~+y$Yb%3c_r;3Hzd`Lx*^$FZm9qXlj=htP z#=KazSGvElF7Dv*HMx zzUwC9Gw~!0nl5o#dmqef{vX=jGAgdE>k^Jbf(8;G=*1H>xI+R2g1cLAclRU|?(Xgo z+@X*JcPZSpaCh&Wd!PG0>GA#O(WA##KY)TN&Z&LYS$oNxb7d02uZuXsisV143u(+& zSP39Xxkh#iRCWeVyllL(19;EPww{D%b?W)`(cR6_`5{U(?>wDT2TneOu#;Cs>?(;a z2Do_UildNn?%pD@t-7hmQeR%zUl~Z|K5X@)i+0RA!bGsSm&l{ZkAACwo-B)*Ji`5; z7LFX{pDY`nigi&mA4{uLU_;%;mt?ktv8C7VV#=*RAq?e7pQsLOe;_SACP;V>b{)uK zcpNQzd=`1Kf{muTjb;TpBh%!3?V45M&;tC6TLq5{lTq|TT5>-eGuO9GP|Y7<@8l<7 z37w@?@4RT7plnlBS@rL-^U^6GPdmuO<$yWfAl74MX0D`m0$zF?lEfj}b$!RjhIc5F$?>{mhfe?BeivC&TqNy|L*Mo(PmM zVl`d-5wGX%(Fai8hW}9P>mLhz`~nF{h8yci&y^(p7^D~ITg(ED0v})BOnEwEV`IqT z()M;@Z0v+kTSbVjfx$SK1rDOm{y2u#nHes6dMPG3o+a-gi=jjS$CKi{e6r@GTwx&5 zS?G)W6I8MEs=N@dEZ@I9!&Qx!)T0{c?;lIR%HB2tt)1_kplCPam*c%mA;b6tknwDA z9iH^y$;f7GGe&*%dIlWh+S*z&r+rUr>tpxuh}-WUV*NiroJb0zAw&|PJF)BDbr}+_#;r1Ib)UC7MOo*xhMC%ixUP>T^C6t$T@$JyEZVB^B zb!q&x6KYE7UXn)3!FUhb+S!Zys~_88vB9)z{VKTc^*384nzES^m0U(Pf?D#aziX-$ z-WiSl3Tp-LgffhC7-2Z)zMOIEO2k8;AHra@Ybi`UO|DB2vh*m0yP!S4)#IC{_^$Jk zmY`_^yV*Zb;)iNmkbpeMbmk2iN~o#-EryAf{H88QsDx-bvtYt$`t z2a*T4EobMC5Gut*82dVp8WB$uHHC#%zE}p@+d})MXj5tdQ>B_#ac0==hR@vGBxu*L z(I5t)F5ZCVYQh#uD-jf?7Wxx#-1?Dj1ym{E+6)_onrAp$h59FexI+&s3cTUj3F@wS z^emaxw)Qq8`&*F$g-+NmC5#97vHv^*|9k%ZUQ)dho_%MGA`vB-1g%k{-R ze!MPon=(85VE)(3_PM#_axLtM$YP;vS>o+^;Ck^t0_>}bZ(RZ~cA3x#yeNRkBWWVq) zfA#vQjG)gBqnhaEyjOA78;Ox03q+LjK4i2a2y7JBAJ2zX z+(^Wfl)eM?pG8Ebpw6}p;-4~*niLci;`{EVfbxNwLvK)D(@_s~s{5IWvGFEAI$+Zn zu~z9JTmtfp(JK6~lgKjtP5-VnfF!J}?bCQXW|oghPBw>*;t{V#O7X^n{=?;Gw!rp` z;Xtse-kOE{=~h6M2<}>aNr2_iv%u;MRz&zRyZC+ZgtKVJC0vNu{_wfOD^=wZ zuPM(O*AzbjE;WZnY1m)(XQ(zdrxx1lkoGMGcbU-5z|j}^mCBvxlHRyo<@+05D} z_IBR9H|suUmVuTRN3_E^T(7xa(A+#8o0==BBdqtOjB94OxTL3Lt$vo+E9UY=WaRNU zFPY7;tx0cq_1f0%Uv#KRHAEpK5I?*D-c;oa8jX)7YbLdwaRVQ#U9tyi|9b2FXS z^Stw%jZImq@2uc1Dx$4lbnMF4H}ndz@THJ3-45lUDPhn6pJ&l@Ev-Li`hV&ur6i(X z%|4mQnVz(hybMWF2yM?Bt-lfVmdp)3uubRaNEGY)5(y5w zEmLSW4dB5$G5JMJqN*6}(;6}@kINW34hRW5rWE^lSh*L=mSQOVD*FUls^I&yMeMRY z{odE`zAh|WX&Qdbq*p?pK%XM1~wA20m#?__d% zT9q}C$8w$o#6DI5=2=#4_5=}t|4MLh$FMIv*V#NAos;GsAtXv z{RWVVGD#HN%-Rlq5)@40b$3}GNCr*xYMpI^r%#?h@9(YwS)-_+z-7rBThnRjsVX1f zYec>@PXu_O_%{-CRE*-X;!;wEiheH;5#Ogg5R}t-0^pR~Pq+bs6QGAc9zFnaSpY!< zY?)e@(=8AN_N$$3!hEqnrLoDf>5_tjqwdEKGvN9_do<(tb01qgulp6~_R>a+Zr00E zFd5@0-k}4KficO+da9>p=CF7Ivj{+NYc!1O=x)>RE8+vsIh^qcq@QprcA&qaRi^W_ zh`C7^1y@gbMY|{J9UiNhUtl2URrY~G<%T}DfjErsJ(x`iR?Y!{_6<{%%HzdpeA_bV%!MWwx=3rv^=DEHM*sTzV+&<)T0^Vb$aHyh^63p1y%D0uz zcZV=A>;rx0hmu6Tk$FxtENZG28&-I6KI3klWDyAja+cUvb|UZ5tIlm!t71!#G}}t+ z8#oPVw>d**?ndy4q@BcHLEd7U;ZfVLG}jGSr#T4{HA(-d*5qL5*et2lusL448D4$3 zwJS~dsMvQAp1;m{6*Bq)pOt!K!6z}U**CLU*!N_i-#hWue2(2;v|lQeU*1(05t^=X z=${e6XJiUt<%s@!U3^AA_;N$I9o+mBHf7a*3bdS-x+}N25IAnyaQskAN<+7#H26s6 zI$Td$CCE)O&&>x`gK&S&+Ost8GdfgE% zACNp3fRO-^x-Ec)QBhO72J{KQAKh1c=8B7p%gDrZe0&T*&!E@k%smebcY{P`_w=P= zj0Ba&K@hj!kp{oBl+Rnv#M-mB+11eK9WJ#p)5tm332>KrwJ}>j8ISdKX|{ z!3Vts2nqA4bpbSqs3!O-!r}aDSjM#aay7uoYecFg`rj)2&mm*tDvZ z8{qz-Xpj&Q(f;*i6re(VPzeCExcQ%=*ZWi8omk&k@1-4QPizBEEr|GrQ4g_#BJs2% zlt@bI1js+}0S_EmIu2HrHY5?~J_c%H0H|api@4L*pM5uC5)v%&gl&C6+W34P+{Ecc zUi?_Pjp58YH*i2M-=tnZA1U3Sbl0@^e$B!3Py2 z0Rv9}07JRXhCJ9o+HuZ)Vr#5$Q*_TxdK0Ksae?zjSR2qEdj1f=G5|G>&*fy}IW@2; z-#4PB#8edkwnZ^L%+JJ|dJmjC%=a98{?VZ?95q@q?e{ayC@vltzk9!GbJ-D$x2d}i3XrsHDPa2m*NE_qb8Dn5 zXv{8yykFd>!nik!42F;e5NDOZ{36|kdt+#4O0~VL9Wc|SFUHj@3D~W^stnG>u}ozF z5+Fc(^Xar9M*eQ;q;S}Fu;V=>h7{Ah!pAqW&d#iUPv~M10;C%cS31E3=^Y(v^#rH6 zDRwOVjin*`w=^P9gp{Yk!^H*V4q!bN&}TA88*7G#*ENJpukMBf*e+|3V|#h*Ho~mm zk}xtxbZ36C=>QnhLr~iVa$VE4HUmip5tS5Ax46A+%9~`qjiwDi;=uQ2N>83Z ze}NmT)ovaXi#(lYT+{*XkKH{YIUp$L2885&xw+7j5wYJr`0gJ$0M$nTxRxCeZ-5*c z=;gbg3=10<6VwlaQ2Y0-L}A>*wc*APX$J4BBPPh_a6x0ZBnBFnKxA^{9SZ^~zL7H; zToN!lOVE$`km6vDTT$M9X``T7x8Sl}1OjkmLhi}h`Go7&t*i5sAwQatRcmZjad=$p z7MgOzl3`wqb<+|9$%o%&POcf1i zi)r}~nB0TvrX*xz5$+SBKM+{l5g(y z>Y)>`Pc5jEb&bVb5Z=%LfL14|NF`|&0tzn?aKS)sHV6->3jB^h`f52=fY3m6?;1X8 zF3=qS=L4bUC@_ljf%kj@nfbmR7S_CB3@(cW1TGD7yF;P*#Z2$@> z4SEO+xx+?4+oYhNU=N6%VA(*D_&ifWRQCWN74Q&>u4x)Ii3kXiQ&ai$y3pZMmx*9k z@%HvU(VIKLl`b5stE=NCq$eZG3ihflx**;-y#k{(>(mMH@!eoJ{t~h05~00gW6^YU zHFle_K%!Av`Y&`u@n25xS1&$!cr;1BMSk(3e$!D+Q*&U=Mokl~bZ&40#73LQp;uQf zrcEPUQy_tyvMGoFOcUUc5+`Qt-Y83vgoCC47F=$+XM6$scY_q{9KeJRNJF{#Co)Di z?T9>Y08_f*wOZ>gvY6`M2J>L{-N?v@QDtjut1gIkm{<6bc2#JwBx;jmW6wciuBWH> z4{5oVJy77<#?DT?Os5?b6{)GI?KzX9)h@5Efxun)+Uvo{$Vk46#mAApv570YyNxcV zCM%tiY7R@ko>EWd`J3D4;`YYh;t>cV(+O!~73Vpn|P$mJ04?k9}4CkidTdtrM4pCU+GYMI@$S4Gj%r10_>J1JHOkJtaf4 z1)x3<<`j?XGfXpp+`UNAlyIh$Jq=)&M;aV0kF!4mS1C#9Fytg8Dk_R(ng}SS=72T> znE8sl)AwMNU{O1EV1U48ISa0Yot>S${HWhcQq>e!YrK_1C$Q(EAR`9_1Z?dzRW^fE z03eI_D*KQ_UwHFJYDEH4kGtxu6XGfOEt}@>fFiIlAI=={_>ryTS=xY0XSY=ZEYYOiO*~lb~){1W%w&X(c$?&*7ubM zJbCbdKo1B81H?2#@f%>RU|Rw?hK0H@h5-^PswOBg1E7|r0^3j44lz}G2dxJWb{-pm z8K3_=CTl-%@kxBiHgfuKj0H@0=AI)Ls)Ju;+crX z^c2tEFDgwI5+Cgu*n9H#v)PpQt&jiy>7Oq3iW{os$M4;p;`=RAJ@dXnb} zP?-ClN6Q{DXtn(J;ehrZWBTV;|DO|Ead1GGmeY?u8Hp}W`o6VcwZGby_P|pquzx^< z&fIL2xh8V*Gm#~|hp+?YfB)-)=_zxJgSUlwo~u*lDwhf!(MumwW>~p#-)tGHPo)_p zF}vC#HB48!bU=om6O7CK@lGSp8n75o$?fIfZta3xP#%O^_W131~)O@_I6?P26G)rJ{VUR z0Y-Xix9C7#E}qVRI3vkN*4W555)GUwvz^?^9k{V=Zuo zCrBYRMt<*KC@P47zZym7XQxs8^0hFMw8YEx#ed)RfS`TvF=79^!o2(ANf!Rk_bJ+s znlhcePSx%;noqUh`c7f%zf=SaBqiXbyB_htI{$rqOV zqbXE7+AFce-rZU({^L`BxNhJvbb{V4Y57XQ(fNH&LsK}5eGk<_LS`3g2AGQTW8V&F z(Oo+AzU1%vmKpDQ4`tl!xLN=AP~iJe`<&0i&_PzBneyXT8rdeT*Dw{Dn8OBq)&|GW zzak}k&_W0&*_Q8eo51Lqi#3w}&FatA9Mnd(W|H#@o7n{sU)Hi)%pBzTCI&|1q3&VN zHyh#*uZl{3LO6f<#{q#+zCM_&px+%EYlazhmMn0;1ojAnTMv6c!V);0bu)8XuMq!u z!|wi>%E;E@@Qz{3yp(#0Va4RmWIpu7G#VduJHPW;JhYw0BOA8ORa^1L8;4RX zBENQakN)b^x=D%goIet@Id6fh67odfw^y$~!ZueT-Jnl8EZ1Huz@~^pP;0s;{;C^RLNT`Qwy^EOtCZs1b_$eXR!lYMUKta3-l|YZxymksqVk~wp{{&>UC z(hl$9FjffaY#ieLz~0k8{shjR-uTa7{MSYQe%1e@YZPet!xugHKY8WO-5t~qg$Lic z8vfZOs#iE)rZ~FpGnX_v{$q0v&zb&TJoW$m={%VIh?=#}5pyaW=6cbl$t2K7vTO9m z9jY-4?icB|uK-;)U7DSXs{w?cwYAC>3g+fTlDVW10Hv)^{t>co;M>&~J?f8}`RmDD zlVl~_Bt($RA_x`x;*vd{nKxB4SAMJB5!5%m?8db8BMJ|%A|zFV3vn|l*d;iA}EY7A>k(j}Yy(ekf8ru(w;T=B+BDsU$xkT{N+699&}h zrPk4|VFyZJY(I_kDfh&MK(1A%thF)2P5m)vl~%Qk=~&%FiGiYev1Q&i)(|Q0U)^5& zwt?4SDjq5YKPVxH_!^ksBP;^v@Ts-%x)1w9ZvcJY_UuH@6UgJeNOtwxF4!CsM#NDcCR<2Oys!|gBxsd z@`Dr-R>2pX{t!(e(_ts66%vNjJg7MDb^RE7giME+@NdPs`7A6s3Y8!78{#Lk1;g}s- zeZGpd1Er;VmFluG@wD;=#J*yvy9*rztjs-Z1S%E#J*bga!`85vAlH&$E=$L{2_~)E z%jd@~yi(}<3+smsDgH>Gw}L#T7O&;-nt4wBMfCk#M-kB6w#g3~_2(`wN+yuD!6nru zWYC~Y`8O_lpwY7%)+#^|`9_!v$EA^%k57vvoVg}lv!o<1FKrFz525ZEAjlTlgGG8y zY#m-)DU?3~RtTMcIYWGfr|y%DDk9nvp2-Uomh^8q6-4DnWRS2Xf5JY!kU|a&{;n21 zOVoyK$^BYoJCkmy3h@jA2zs6B*;&*2T~mFH2Jf=+p2t<{;qf&sPBOc9FKb(52jzg# z!dF4xJ?Qoj<}Dr{pE==fDVI&vj(Ybnw&MU)-Yuu%dW42&xP#<|i9U6y1ZvB6%) zT0+zC1nFN_(^5X8?IWzMO&sh1<|P11mfRHd2>c0NButrx^Vov1dI-+MDkO2Am0HV%S3?ld`3t{aJ4=~& z!LuJvbcgFIm-R0Yh9vaDP3&;t*+RK0H!Hf^277x6$K2&qBj-8yEl@khvm?wW`U<(j zv9i%lt$frl}6VA4E!Fj6512_r0%7*0DeV<66QtvSO-P`|nR?wH87WcwCQ60 zDSua`>-SoN+-UR&4d+h<7%-c`v~QM@nv_-;RHFf*Xf%8g$9Tk9GMU^;PBwx!7Y^J{ zsI4cH+FCTvYLDqiGM6#rQXD6!&(9NK`EzCc)aP#RskAYOv-s8P?}mCRKKUBohMQhD z7__YL%gC7-AI6<`+NFvjhQDo-vPk}%E0qmKif>hp|5zWdtKNGzXzmZRMTp4{gWCc}s9%~^)RjeC2` zV*$yS{HR&?c6AqKNLm^tr4Q|-XOu1UJF+7xjLGvk#^TcnmqdtfBfrbJKu*ePeZOz* z5Xm)n|A790nd-V>5)oW5Cyu4C#S2vhyh96Ys9Ebk2|fz^LkP|sK3)v5n>ri>d9%^3tVKRepG;ZZ zQf-gis=5`KXF89hU`@C025SvAePcK0ad3+?Nt7`8vFN1iU)uzmrl@)-F7|0OWLWB$ z<$mHlap@Hiq}uL{vq>%eDPE`c!O`Gjx{dX7xn_D@!%QI&#Nn5>6O_C~5iuiIe8qcL zD_g~H+~K^==e?B_4UD(@KeYPz4yDgfL_YSkyT^U?6@OP%E0U!Ibr#q0?2w#p1k3R{ zLWjR=K_xqcJ6DW6w|5KaIkCdXNa5?)>i*kAUO7wHz;ojKr=6i|do>r?GRx1xMOt(x zrf3sG+1lkzR@bmar7-wA98)iE;$Dlt_2YCizc09@XQT)JD0FovalSBI78zG2?ZtNad5-7c`txz#${9WG+m`4#o2nr6kvtjmyK@1GYb2ieW~TFEcOzfY z5}wu!d{#&4RRA8A2Rm`LBIEVM7PIBe4KmZcSBj&{vL?ZK_d%^S8_x)a^?XxMGOBtm z3fk?fJ%avG>@he4ws@8ls3j>eT@U7QpYLuCHYMA)kikFZSBeHJwhGeZoVlCX?E4zW zyRV#t07Fwc>(hzZE%>WlWz4vN-pv0{^u`MANQdgS(os#A|A{Z@N=K_hQkC!x*Yn|j zWALwB@gU?+N=kb4h$d+0CI7080G-8gHxZ~pRxYlucN)xWKTuIf4(Oo5p`fQ7m&T}@ zUZp+871E6F%c98rEQFZSsiiYx5B3N2cABMT>BLem^g-^f#fM8P8|XQj4dnO7sq)?9(lA-2!W7 ztg#!+5cK2$=4V2Pmz3K|$34mGY{D0Ic=7Lt$6L*iaCg(hG3+L~dZplmxENe;EIZxp z!_%38;);v$ChD2!;G=BJ1RY`wZ>Rom?QmMp`U`u!2J5=jz7*SqVVjTUVBqafqTVDY ziN_naB)%d0mk(%_#Hg@5_%wdxu`DF2)2bcZ%13^Fu%bLq1dqO~iswtYK3n=GRdx75 zyta-xyFZFPQxJPCjbFje@t=d~2R}YL}-89_pyQ<6Id3`UX1{2ciR`5OulJ)V5BXV>A3O1zfvi&9Dh z!7FDD#j$rh>dwvga!t;L^UdqX6|$OfnA^+0ZZEb?G3u_oN*J#xT(~(%g%*F2Lu!wl zCrqQX_GvD*I=MMF$vDMp?Y?<`(@+cE=Y)C_4L=^W`X zoq)v~ZXx)DG85kKzDf8Mbs6%=0F%FK^OryeM`0pUey4!c?Cp_&7HT#OwWCc!?Y+$u zX`&A-)_aVDc~N_N06_@N(XmN8nyE;$dB4X2p4}qS5;c|}L@~WYjqKq%;`4c7GT^G* z@w_QcBT{m`iksNkKOVjGeAxjGBdC=;Z<9Kc-QlLxUKO}jtO2i`W8-o##>>{xPEPqQO`amxxtbJBL{p`i=3eS2IJ zirOUl_%+vng|qpxFBTtl2eq~Q`Masto5Hr|Fk2;Z+Wz9c&xoQqC=_IT7sqQwl}R$r zd;a&q$5w|D&l^oY*Oc9o`j+>>W!>CQNL3fh>qiUam`Q|y1BLU5wR|TbgHb`sPitP-IPuOmyp>}C@MCWl(=nhW^?)V zFg*E^>kNBXLNcZB&GbTN4p&5B*n~JJ>HhObJDy8{)O-vT5quqY#r#4rUudwP_kE`9C81zrcu-YG${o4?t&3 z%T=tM6d5B1`Abax*BW^Ch5}n0{(?IwxcEoCF)+kh^ozLK}e!EsYOYit* zeAs34Hn5`~gG7VcRNmr|xmoR^x{n{a4V{49UT{TUHBZOf!TTn~LR0%#94+2*wvieD z=KFakC8;qHiwZSRc!Qyj7C^}H)`g7Lk{g;3-*C{eIvD4>EaCLRO;KE>7n*c@%1#k7 z^1VT)vzGk;^Re893121#<#T)u(nidU2W3#1@%rRY(Kx|K1E!|SbGe+IrRD3LytTgG z!q*bHE8WC_8#LcOoQHX5YPxE7dR5->9~v9naMo;5!92!BC3pm`F!>EmJZ(-olMlS= zr6*q@U25Mk)JKfWHegFAPXy9wIh3u++s+gx)+Oekrw-qDUpM0jwm!G1637H`2usMiGEE`>`D*Ll7KJIM(rbYM}ZQ7cvi}*e1Zf@bk zASV9+_Pyz$7bIjam9NP+29y3w#ntI4tyZD9?jyo<&G67u&pl7WgklC6fibO#vm1N! z(4pbmwX(vv)W`kV;c|%|dr2E4-rzGOpp)3yxEic(durZ>Ka(GLo%Dqc2ZTzqhda%5 zv8m~6pGz)WBlno>WM^@9)@5B9zoD0(TTRWk`rzFa^94BG4!`oST|3^=H&NUF{NLie zo&H&Uf?RKTMaAX`Dv*Z+E$N#WbvjEkvn+kW&~>2ai-EBWhYL|Uyp8VJ+1ZiYvZvPQ z&ZmTAk+EdI@9rw5gq(;RFs zgfkbM4`0DSr$?#qN(u){@6Mt$wHJogd2>AHx6&ZoE`)gFmUMTua1hxb)XYuEOVP(4 zrGN6_eo*OzmXmX{$`E6Egqq*4p#nSDVd#^qkA32YUfkpyA=RmlU2@nj5`iBk^<`LtwCEue6VJ4L%4FDPv?i`7 z%v#LhphG)LUg+@fas3n;+#Gs`y*M3ZZD=&wD1JM6aqZ%ZmUW6-lrZp-7?K4+)jtv{ zRo)r4mNQZrl_rmvi1WlsrQE<8@^a>2EPlr;PhR(WqQ#q}RS9?RjhOC((Yd)j?H=qv zl+2qBVNU1_${TtivY_0j`J!frTN>;)EIHhjYOoi^z!`P)6B68<1ZFBS8YU4d_$%#b zT%3>P?6Ab^$HpC%UgQi|aX_Pn8jLR^hO_V!Lq?NCi!Ls{$hlc;*0P8T#s7RakL^ix zJ=2jMDiv~a%Dh`goqE1lTdHBY-*ant(-!`%dOkd1`=9v;Un!CMyrf^f8FcoeDpuXXkS*Ph%bY1!3nZ%MC$E;AH)r zIUMY^`u_GW{;r9AlsHC;A*j20)WKNpik3U)os$WQMo}OY7lU_k-2&b6ZA7q^&}AEw zezxkAX6neNBx>%DN<0}z=6lfIl-!? zexyEipWe_ryds*c2-c85-b==rO^k6&y-g6|E&I|YYnk5c>{=&kaFj49Ic0W1jyNCD zZI#)F(f0{40;AIXAdtLCc|7wI%EYJud#Vcy$EC(tixbC7h&WM(Nd2%z@$JV=TIOMI zzt(j>5>Bj8bT^K3VKh`-P0;6=GK1M8nKzRDTlM}hp~u#Gr3eHLBD#^0?m#hzos%+qv)rYbSAP$;nc%S*VjUW^r)XWkmGryyT}U zK+&4h>ZDSE1QBi#?JSJH_Gr4`O?{(A27#6yxeYqi_@OtocZ^jPcvBC&nfNgsb~unh zh=o`4-C6ScjDm;4_>mTzfqo(3ofBPz-Mw>A{ad64X);9AYn!n)4>0?6d57oSY7t}{%=*-0oh`lW~x>%|K`k*7uKFCR|J zV1b8;GZk%#OP!$^WYN2`-u?;0MH zN}%kxmyGmIB;}!pMCJ&p&}A$6i|~d(;s$@ALW6OgWgGmy2C@hxyymF4?Qrv*WN~t5 z?R8S-H(bQ4=}}L`3k)$)@7+IvRM#=b-2;}7VHTq;sGJjnJ2BBslhv~!&20gL(pd`> z9jcw8*h}=&Xgscw^`1lzT`jKz*t<@KSulTaJ<6d+;Sr~2?@XX`#S z?fGH0O(?Cf2~8W%7A&4GZ!|z&+!^7492 zESL)$&L`~H+D4&{uOwHt=_PBgqm3tG)A>q749bM(5tr5{&Dyu_&xr-DuHLisdHOT# zLBb9l5g1vR?K^o|ANAoOTybLZ6DGXs79&8MrHx>!;Hqn9C>8OS^A#f zj+yuD5YsbHuoHKM-Db#SLS=2{FKEM|K6kmp1@WR%P1^0bFGQ@@JiQhnVafT4GuP}E zzuLd;FX?Rb@u@U#iogT|yp%lCpVB4b?Ywaa#@o)IjQ^G43`)oPoE(Y^| z3FN*&+m)IzhnfpysuwV;AD7+vu&gcLM8-W6F5YFcpqKG8HVndqOxk62Owv>rRy2f~ zw!m674XOAhn@6*z_HMJAiQbVyg76p5hgPNeyYh%V&?Jy4AxRP?w>oMmBCdZHby?<6 zAEJeSme;gUhVuUPSK8Vb0Nb@DP=QWbhgeV7noWI5bmJ55yr3Ny{?CK8IXA0HHb%j| z&MPN0puWpcy}2>+J67-q{mt{yY*qfx%bu6rr!F>!1%4nrnB(nT5pSTJoPgzPp_LdK zzQO3_;wocnf`y_p8aqkLPof|VJh|ZH7zFTV@sP1*B7I3`MZ#i^|v2I88oD65r@ z@Og>n!OdJ1OUmO9y;ZF#?k8h(i5N~9JYrHOG^?+>GZNJE%kL%v7ntH>yk>vZ2~`N? zeaL*ZKt!nQk$#}((#>(D6`$wlTv$sHnlDAqA`>_`@2n0 z?i;xO_%`Kjad*Q~^R%16dUyJ5{Ly&icJF;+$U%Q|D(}JCm#YOLV?GA+Uw3$KDIsni z8*||3@v>LF-sZ0(mKE_;@lKyJ(1epI__N*IT8Nl`RB*p^q-1?xM~?AW*229f=B8k$ z@2JWYEt@Y$l8%y; zoXmmWpi<03D<+625@Z~ptlmUH0bc36kRpp|7@1E zR8+qGLKQ|^4mKZwE9cZJYkA$w0VnNvP5-@4#~{)7+h_~k$K80{wuV9p(5kcGdC}oJ@|>`8m$p|w|d`^D1Y|6D(p*|tF%^* zTk+~fT|{PIFM~AmpwA)k|K*=}7` zb6}6mc#<5LX*_NgRD8Pen{(XHO{X0xP=q?9YEK)d-L;SRkaCVSpqJmy?NWQ7JD99t zz3>tG`0Df5&zK@@5Q!Ny{fRYS_KB^Kx*IMWtFunbO--=7URJJlCT(>RW@2xDieiihD!PEMKzX;D z@OMk-`F(#qV0p(fwPUSozh^+E(2-G{`r>CI7{@x$6N|y0@%;&Z)6$E&7|?iYgFNp; zc%jzc+Ru;Sjn#qGzhL9I@=}NM{5yKN*$u9NyCv_M(HisD!LlI6%ThcUgWY2?% zAxhtP)EAiizua{|O}%cv=M3)Rig$7G)12PSGf&V=`KHqeCmFq5XZhJTUDz8DrNObZoIXm&$~0 zsrG1lE*=vCePgEhkI^~nn|UvpR(MWA@#~FFT!{$Og|t8)a|IR!{%80%Bw7zUsHF|q z$Rc}`_AXnz7MKMrE4j!xyBmLzFw9y=$fQpAvF#%j1e?hsg2vL3_`N z($vTZ=sMceK>?1~X`KDQkmIWtXpULBdV22|cR;}=2w z0N^P^PPP_xO^A4lA9Ae|5Mi?QXL(4sZ4ki;{ZDoctP3B6ma@Q_*N&032x9wp3Ai^R z-^|1rc1S?q!~0`}8u6A1+w~XF*;lO-eI%Umg~bvRGM-mX-S7Gqr?p>O640Wa`^1WK zS|>VKphP~l2GGg-u29{T{bM)+?!-9~-rX9hD}o>j)0Ep~mQ?O}g1Ne_=RRdB&QV5m zs)Yht@Ho~=san89$S&_`kx1$};zlR_tJwbFa|sXUzH2m;ILnbZk=lMHwT2(0PREah zJ0xASSGn+ruG`tCKYqHV^f(+!@z8FK7Yldruo=nJ>Yv8mAk-PTOCf~KC!9Ec*CJYh zb*?{$qp4gid8rXNOSVPCb`;|eo0nV1V$I)PdWz2qib443aXLq z(~pGdHN2L>cw?Wb3$rIcYTi`*a3|4TQ|g4A%aj4QOK+{CiC^Ht>Qo1!d*8k==;IfO?|`k^t+ugGds0)*5Ln9Jm}xfM9OxAoof|EgRDP5z zxZWdp*{-sHnHciw!Gp94pzPeM$4D28hk@~HovlHfGLV32~p&@A{C1J$pC@7!)bE!2D|Hl6Fe+C$9Fbj3Y(eFP9+!l$U zx--D}+)b4G8;7`vN{Noh&CMOxY*RCw+?4gYC9I19lv?;d+Zf#s>>+(XIC4bW!_!fo zRo{Sce*j0U4)0xqpkL85{C#vIVX%q-DSa2+5R88Cm1}O~Igy8w_#`rFb}{ja8IRVvkGSI##|KbTe*yvh$Khz<}Ukb}`r z_qTWdlA*sjDiBO3hv4_4yPxURoNZt9XTR>*(ldz^m3m13PW{WgvQ$^Mc$sP;TYl(? zE$XTDR+q)R3f-y**^8n;(gv*{HP_t=&EoD7+0kAWafzU2+b~BZZqq4!lr@sq$0JkU zlF>;@OFpDYmJCvvv&SlFQ7ce-*PVr6)E`@(rba}Qj8&CC1I8st>E?o9ydsrZs~YdA zp)K2O5Sjj*aCh`4oy1fgZEZ1BbD?YW0@2xD8_VMKKTBklh@8{4b_7bBjq~qQK%)dq z0s(6!Cdsp$dxiV)b&-{LiWCDmRW8Qs^bC0B?@%=GMI(I7R>qg2Zxgv=uIN}nL@&u1dy-{6WGt%kZK)a%}gOf**80z z7i+(s9Cxg-^eFJXj+>C$+Ge^>Sw!8=RN!6El2y(=v0rcGCWqR(jyMqQ(qi=qk~+o>)DJpzYD`P7Ix7B&#ss%o`UV6?sTg1@T}M*6I?VWE6*SK9o>*lbeE zCR@-z)06TEclDjz;Yz;fT=4J~qSpH9@iJ$T)PGh^Ls^OFTe*)o1yhK_d*J2(@kUUG zQyo!JwYeGM?@+1&Q9(6X_wSK9$FesV{4vfq-aip7jJQ;;f;PK0VWZ1KELQ!$sQSvN zxSD8LLLgXzySuwPVQ>%b5Zv881a}V(Ay^0&+}(n^yXy?@_ReSb-gg#1fHe$#&UEkI zUAuObd8U*=#&)OK`t!Qua&Wt_9mE#SxQY>Fo?F#Vj%f7fFl|cV-i(L4RC5-W2u_BS zz)5VYvIoCZ^=FeJP$3xTx!74ypNz~_m_*)lPgSP(c^H;AGf9O!*;O*El<@Z%;@Eg( zbH=#;JLHc@_F;I$K#8C8FSiWL{+3Lh zF7v8(uZkA6yj_8+MIHi1}9Nc%vCru`xA5WRgaU13us5KaHv#{R^}$I%)qL#1l|4=sMdWf0SS=MR&y17UDiv94yJ8*O?-{585f zJ=?#znWRaBrLi(>%r0Jum{Ut!Q?~k3J4V-7McQk8=F1e|WUY*J0@k%D-tyUMXlVCD zf}2zd>uVL=c=cT!NB|P?cDBvr4r|E|EA<9o4yuKh#mFz$t&gcscmEcT^Le~$zaZ2DQ>7Bn%I&XW17@;N9L(NL4Z!Yn& zZNVAl#r#Nq|8W7bQzmjFF9j3vJ;q0hLZ3B9KkKFr&P`IX!_a^v|yb zzKGCwnyD#ON(sw2Sk7Q+FwEi;Rmw5jQmI=?e82}uS2}KD#TNS4(sA zlt-U&Rcfl{@FU>ZEnArQ&CkzIps40%$GCm-@IqL*qW{{%j z?2JJ6c6%k1+S~u$LPoMmzG6o5v8AB5S>fP#b%l2SUU0Ca?BNPjqE&%>>vo7m*D5ec z(K$V7Y*GU9p)J304rFWrRm>heuMqi!B9Y+jIph#*tghZ8rdat$h1`b1cx$iKAg~z8 zDNVdO)^1rxtQ1`2tQmOJHD>tgBk1?HQVf=NO!S?7jf9)Z;$q;#+nrFNzsmM?q;JXHCr;@UR<@Xo zJ8pLJ-a_(%PP(lBFU^`T@d9iOP)A z2Uq7^PiqPGL0fv+gz^I+D21~`ilrtsT2aLz#aOZ)gq3CJrHy*-X{pmeVs9DETOBB* zh#y}>{dMDj(gicN^r(x*vMa_X8&|1{aF#tec3D%M@^;;Lxl`JCBO$^2SRTTXa*qO} z`_NuRAqx9H_n1f9n7gNXHs8{M(FZxhDrfYElFT*#+$&~5^1iMq+lyhWW?JQz4 zkO>Z&sEN_$2hpKK^$g+QE~rF$4tIC_l!%USVsy22KV%+D7x^0+$o&1ehufa7*Z0~+ z$&$GvXrb&-8ZSjzEAQC6S)6E4;q!~uZXvW)Oo4LcyiW_iI9%fhKH3ght^r z_dJ@%-WPxY&x|t+e8rlD3;X}b>=V88!?8|F)s!^_zLsCheQy(RFO~O>ysv9AISPl0 zRO!lX|CHO_f%UG3(v_(xh%8(v@oQjnoGW>>@Xjn353?RxWv4NBA-?uVZ{l8wDi*wc z(A~BG2Vj*IPhrC;mL5i}AyQbckDyF$YHFla^iXXlVJeSl+6vNui$mDtj@L_W5#9hK9Yb-)j}=7bdKFq~VyxC^4Gn zI_dNY3y?0kWL6Gl8gs{VWR>%KDYzoNRZ-y7dA`)G0AgllGmX8Vuf!EtWQ7HrGA$b4 z0(1BP=i>fl%Ou@ctX!7n+c2}ku$Sr2*O5FhNxc!dyj2K$(w8s=o;P)pw=3Vuwlaz+ zgt964vMCn*%ba_jJbC=5Ni2YR^2%MP!=ZEF)k_yo7Nkx|NAZKca;&5$I~vrhH!?$uF>I^8$B;dAth10e;C z=YX3rf%X`3@OHid)1U$*)+YjtIpD-!LK& z9e71t%o~A0Q{hxuO{JYUy9{|8D4Rb{v6ZWEy6_cwG2zNs1sWi;>vGlw$G7i>EoO5C z(}rskZQhT;&0iao%$?Xz@N;9^JS!S@3F8N@Pfr&8ochky0y~<;AJf*n8j5r-c;@1? zFNL!6(-$9OMyT)J^S#Dxs?URi-7c0+4rQVl2N4epmoI6c;GQXHZ z%a}tE0iE?mrXZ?wclVYvR@Ad5DI4!38ufUKfix$hpc{=?Itp!VPF z8@nca2LKyD^%}T9WIpQcv$9M*7W@Iyl<0pEhDsV&gy-!!Rq8v^(v-D}gf`Q%J}%m{ zFHLK#S|d4DzANv6P|qTE&4_!2r&X(}H7xrkWmPUOeN~WvCbn==Jiw}%N!m7pCqoex zK9_JalOw@F4C-@PhcnXxqh2)eonNGVI&yCj?B!UWO9T6by?TIr31#-(+5ROZQo^m}y{bKfNap7l@!8R3kVat1@>t$Cjh01Ov88~p zoXxMjC0qMm+>WQ*otCl{Z!q$XI0P7G?$fV#F<7-J0e;Ff$EAjmbOh@chhpz9FoqgM zUCH#nzEWQ&@lesq;*a<;Ti6#l%h=NjkzXjT=BFem(ZpIaLA4>nbdoA>Nz-JNa8&jM!P{0t`|DIpp-wzHii`}1B3DL3$^l#( zdHJA(uCnS6N*knE93_OF8c~^``x2IgajQ?7M>yzt9RaFAc7JTlC5r~$6(n4R%}SJ| zLp&h1Eyym$&r>6^3f#j zk`R&lia~YrDm9if(sc49{SlLi3tGUF5>@Hm1jH(6&d3FAl792V6;_CpNpWj+RKYcS z;Kk>g|AG_B!YYgn`klF9F3y0|vW`_91+`il7DJC}X&5QVb8YjQRNlbnItmU+ehLZE z4%%TIMgh4NEv~mDtnS6B8UmEiV>_OBLOrOI-Y%;7UF*`8Qv$07>};S`U+cRWaqIi9 zYg=CX^X3|(-`9$}g#X=4fGwr-zAO0l>;R>7_UPOb6{N?kNs3EJ9X)B;3nBFtHl)8M zf$V`yUUnuXn3&#W4jhrcTv@3rw)*SRD8@`&Yw*?AQQs_SOf=r|l8vZlxnn^rebAtTilRm8$Yz!g z^8EWN>EtW2yB=S$$85*k&M(^6RIkzp|cw|bOyDq;$h9660z{_K^*erETkFJ{0MW#1RJe~~w z-q^SX(N9)%N^c*R=?5{QOPW&bz2NIqIt$=VauO9a9CZLU*o*?E5?E@Pj<8q#4b(}zc|qJEQ_T992O z9n6j8VLW(Qr_1j6g$_M0;B~6@x?oK#{*~d#^CXh8BgX5n-<9xBtoT98a|X*zJw-4# zAw-YAP^ke<__M!IUXQhuaq!)fm~-nciw-HNdz!Zp!x=A8vltVl-4f|?z<380$SkjS47Gb3q;Im z{+LrToIQNX%;zbQv}^gmqTUYqfoo0}2juG#!SJXcvr#`rVH)e7KgC}gnPZS0^ODaU zv_W_;W1PtC;!7;bd7zZ&xYq)qwRgVEVa&Y&=$ojynB{{ZADCS@QaJDe^S&ve3}|?w zh}N@rH|Yf^j|eeropw;ZaEDH6Q*UA21P@ofUsyIzbL7jS@|vlsIk&8g5;G}YwY>g( zZuRNfW~IqahH+JzIQ6o#aAu?I^q^*^Nqhezyn;gVv^5-vmKoGaOM@vTYSfH6dwtSk zEeXSYxCQZk39U7%*PwnXsQH@&BGL( zcN_(?0_QCsGa5a~`wK<@fv_@>hkU5w==(P z_A3+!{0VcSwGixC-PyI&zxVqUCZC~!kUdTw_3_%K?Xz&sa zzWQ;7x+_U#Hk|$`;IIxJx_A97w3mW>pM!Y60;fqeK~jTZ+JxZDXtm*mMTUt-{+-0V znc4@wpz*)dE@-8ygpPoj>?u$4IIg7_0$;E$eyz z;u#yhTY3$e$T3Gp)+cLS+vk8~I9$bvd&{BguClT}HgV2y*<%`KcaP;ZLL@P7csbnBLPFE-L>dGkyWTw?IzP))bjot=V-_V6X z2dUfq-dyMX`Dz=qnHiPklk*Cw?3$|f?bO+tk{d(-Tn`kGr-ET7Kj=ko5S zI_OQ@_D!P216!*()l~_}vWduSIviU@G$s&~7gk1gNj2~ZBEo4foXeTJ9Z>V2EGWw! zT3tNmfbc6qd{p29n@+Sgep7qO#L?;{RTQ6~fnVGZfs9p?47aqRr!wDWYrQa0kqj%E zs1%^kx7OtBuI60Z>rl7Zx%nH52>(O`lb9S8;UT=;OUNoo)_T;1J?zf%Jvb+pUJu#0 z>AK)oi4EVfb^bGPsISs2;v1Wf{Zip)VoOG@n>Z^0c*K$O?FQEFo@Ss5ha<*}MDHV} z9&ZmvRN3fwwR&xVmQ{nVSECKdXu~%aACfM)B_Ra#&J&`k*ZN!MUfk_0M|| zE-v*#m8u&+PNi(tde@N`78X`azz67;6-C|L+^niH*<3-m>$7jzCdf`p`>96%cH!R^ zY+RU^``ZAZA)N`Em6uQv@KpGI&4q8hdm=8cY43uXt^cBFnq=BR9nq;gbcgcFe4e*X zrNOOxo5OY@SUyk`KT6}8gt6P6=UhNu?-%Bt>FDEaN2_mcJ-VkW!h`s@G@ znONSss-64A_6oi2-z5t4IT9c>0ZA;_fp|YRm%N#{-dR;1T$mKV5{fm(2J~a!wyx~g zQ%g#hAy+Q!s}o>yr`|^Cg9~H!u-lU0PhL^i?axbJVgH*J*RLCFu?x2KfY(lNyh@4S ztAE-*vmzR4KQm0q#3a8DRBoxLsE8f$&hSpDm?17UmUwqAMo@157uLn0e+Qy^Y}#hx zw24CF^VR|n4>zQgl{{E@b&=mU$wLlDz_lM%QKII$(lG1#E_ecur6LXEdR!p}KJOLF zP0-2A*(wNDmucUW){iL&gD7a!KmmKIf`J;hF#s#Ky)34aA=vOXWEsSAFKLp@Ds= zo2@8CVl+_$(+L0m-(Wt5GRDxAUsxET!2)PgLdy(Uu;L!L?y{%n?%wS4dT9@}mR0E@ zQtr5rbg(zwr6jrYlyI~lBodl)-otADf)61vlBC@jqI!ATnzjA}F2yf%r<8~c&_qo~ zm61@T`^9R{2qCmRO|Ve5Kj`4$X=>Zqq)$pE6mEW59 z81(Ox5bT&)Vn;6f)^6m?gXJD0e7W-QhJE;}I~k(;F4yOc>cq%4%SYlylnRMqQkNfZ z9jpfmW&S$lu=WRsgaAfPiIf5Y0@@ic*~X}3rtIm0#3;Gzk39;dg`nsgoVfYzb5{=! zIZE^hQ>`l0Y{lYH^B$4p#+5wJFMNc^Evv*pq1ZOyw^q4E>M&K(U~qSrrt7r_7)Epf z>|Q)ghaE+QE<>90j2zZ;=nB|%{;nV*k;?n@eu)M~^fL`{x;%H*f1*v$`%m|ka`}@7 z*WRt!@$snNrC7mC$cIDPzKw-_kPGwPLEjqf9cubW8wL10#r&u-`KWX6#R>$205qmM z-y^vGI;0;-KcJuSB(pgDQ3AzY<1g;6qFL&|+=PImiuf5sXmO;{b(L97P}YtRo;bCJ zmd4e`Dm9GMBLzRwrWp;Cr{XV3C~)`q6ZA^%_UO8OCKd8~K;6tp@t_XrtK%iP?~lCb zXVl$Wl6Plj%IWMHm0Z~_P$3|MEp9?Wq_PcuNyl^I#*2Ieja54c6V z>Du29HU=izFkek^{IDoY1UfloAg8(IG!f{ap33R6S{*(QPPoi+f?tn#D<&8J)&JUY zegh7+U1j|+3K^an`wlPgjWE7gOL2bW>A~5fG|GKW?7Ocy{f)jqY9||HHcq)?-nlAX z#F6|-iS(h#MT=0xo_XY4*vEwGM*?d3*w{t$$A^Y&xVGy9BGwt1nN{6rfXaYor-)H7 zh^+0i&2qh+dy5c1KR**wc|idTTq|G)O~=RxP&GQ9E|5U9zyzV7=rOOcTW`-6Rs$B7 zjSYjaroX?i{u@wL&wm9Kf4BmPDiE?&><&l<0&1r~7WG|L z3{XW>RmI312mHZz0i%1Ma(WO1)KUTa;41&d502o*uRWW*E_?6W<==<($N+BPqoZb# zsEWldfDR^n9#C=({P;;(*{DI4*%)v`Q!SF;Jomi5z6Qix$-E8R03y-v-|vSuTt%ye|3@PLJe$pmHcKT)6)D!PNoR(!&`?uz{< zkoo=o{iXVoA`k}hM{iV{!G-#Ur*^l`L$6`DHy*UEriFG%sMeKh+J|$lgib2ZWg14%FTa! zvcl+YFvRWmkqFC{kx>)MJl|#y@C*pk%{)6#RU%1`h6G{|$;Q-DGqsSs*FXVDlM_Cu zNk;L+2;cr;F>NdyYey$~l ze?f*+NFx%9C6ggs5!q3aU&n08V5zREaeD78*8-X>8Gm&-35^UoIKChA6L0=sNxwyf zOy94SO>2y7W*^&dHFz|TcdmUwZtqF&n)o6BU;@)n9$Fp^jhFbV>y@3n4UQYQ%1A6A zB!=EgsAu4A|CnSpSTQlA6?~GRU=on zL7~#}Qe(*^QYS?)7i?=Q_6U`)&w0=I$yPzA$~iHwP}*5<710EfQsv&g}p zR8^+|Lv(ru201yobZ~6ps2;){OE=^8W1-$xK&JULz4u?}3|07J- z7|%xngA&v=F*g_5U}9`cP7Yny|M!ei(8waR;9Bro>SV06YEZA&Mgvvlk*_l%OO}RN z*`J~BRN-TcIT7JZwh-D{#|7>!Kl{O?DCXOMhl@2jJ~;zWOWN*q64t`6$7j(GHBTKa zC6BSA?fayQ5$r26x^k`~R2AMeEd_xOC6WXyhZD46r3xcKo~=7Pa1VNoEWFQU{Rcm$ zn~^{Ij<@l;&v9z$V`5OtzQ+M{#O#me=gmSAf*@GDXP%v;KTnyy@utHT%?F+NTFYV56 z+oi^R!8D&0fqadq^eEdfGp$8HZw)|Cf465QEiB(;8J4hE zr<@f@%=}tQou7X!(j^h1rupNDc>Z?R&j7{{oL^yPZe1CUb&}6k>k}E|w;4mB9yI>B z>*D1b_TE4?O$pb$Dm4Ha_jD9f{7ELh-VOVKY|f)uuf-*a1ELl3j#eB01Y77?hnH2x zGZ_X*R4>xO|AlNkX0hJ`c-_*x9La0z!);{_dzRe2^p{I4egrt8!pN5d@}xn)mrR2z z4Xwh`19csS*t{m4G(0UW?dj^P0nY3@f!BI6>MnFIS?xe8I8aPd##e5;W_$Rvdn0ANNI`d}(phpDYJPo7_ z4-_%-)RzQR)}Uer5I9n`7ui*I$Xzrx`Vre;MAjw+I-DfQ}yV>U@UjDuZ#h0tV{QFZJ%XyZu2}$=9sfOlW6xt8A4P<*s3a*C#w~I7&!zG2ei)hw$PW|4%9* z8d3kBOTnnkU@!-gGsukmXcGHuQXy)gBLhU)ipn;Aa%MdW5m=+kOv>B3vD8FB+lnf# zA}W|0i}m*5xYjC^lvArt$l^(yQE4kAAalL-F^h8U#%&|ZADvJkW1*1LS+%ce85CG& z>Nwuw4N?1~V)3$xdL87IhCfo~y1R$Lk0QEuKb9`Qq9`4Ku35KOpM0?Nfte6mn7Y*i zibhJN08}w5!D&f!+p6h*UVD}Wb04t;sf5EQlni>pq{>XNR#CD^$lyFB(2{4u|+A;i8XmVHney{Nr~ zmd5rL?=9#c-NB~P5LJjnf6?5LxPlW5R)$5yYiH~-C^4+a!D#~*w6AVdH-~%p+r3Pi zB|$gi^@O%hrTW`87OZ9Q(Dj1J0{EvUzq^qJ?~vQeFHDfckgZR5yZ()mTwTO{sA0#P z{AB>PRxmR4j9c`xJfQ_A!h2r2fC%_)b(M~s+HIJNHQk9yeE3zuLt5`xmE}@ZZQR#y zZqjY`q=qS5Ys%{t`esV4MJrxYufr*@R2;CsiUYNsI&X7R(TpF{J1_2rKH5=KU*5h4 zeL9^6jxN;eBoiBGmM&-n$R2C$?-}Dd56Dx8VW9(;!N8^j=)*63kE8lf#@5^4U*FKM zt%tA0@x2hOZb=EbU@(aWCF=*^+Wcp?Z94fAt*JjC0sP@h&*n(32ubE47NP`@2_;ZS zapK1)7Ed3#CL}z+97~`Aj2xg_0u2of=)1Wx3k?YYueRU}Y$Hw-0iB50j9OoM-03hQ z0bL6qM%2~U^JkA`WZ)a}4csp2>gt}ew1176h=F*4H93r#M-(EvyckB^RwL*P31 z`&Pf26~Bc;-l$}5J|JJAVNbUOj3)qvY6h7^eSN=V|MwMN`p^N9W)hMPmwm;S)wRjV zy!m5582R3}U60v=zeJr25aM;C_$QEj$z$7uMIl_r-oEz!@Gi)KD6D6b!BhU~uRJaC zaI@Td|DlIy0lu-}^kVIP@mD6!z#RmOewV^4t5VkGedX0dH}TV&y{xu+N587RabnC9P`n&iTxC>ds>69~^) zql!Ursz9vFk8Fz zZ|d5c%vddJ7|BRKO1U?|2|i=7Ny>RC!bbUW+fe_#Tm2CqMq+{p3(Q`cW?Q zMJcGypJuGxY-~yui+f0}y@lNKI)6bCHT=rYr{LpD#}<4ldb97ytHp)KP86HU8j?07Xwzm1K!Go6CjumRa@qax1z zb5DS@sCB`H`#xUe9~GO=S7~QtX3b2@={}enRUs} zZDe0OWj;sS5-F3#WJRYd#rle4FMnItOmFst0?E$8V8LOZGjZm#6#Zts`plOgTi<8x zX0(%^N>%cxQ*8=@kOc4?=BfL=FByT&dAlLtct8aA=aTV6M9MW+j4c+Ge6uaU@FvW>cq=l-XVGOHwzX`oq+ zRx-iE2p3(+B>LPNzn)|^U`->rgi$fwq2rMGU*3!qNV9dEmS}%K(u99`bcF1*OB%e1 za~Y^^w5a&NAH#9HK^Sb%$mh41%-Zx*=@iH))*HE8isF)bw^}D?Dx{5(_0!`YM`lZ8 z5l~pfkn4roCkFOV_Ck}hNqw&OqH8Vb{FHr}Y3Nj*fI-ysue1e8G-ipg$lU`A$#*uE zOdh%^qB_+>91@XMm_fA@;z@t~VBNRb3#!Y;GZqd?fpr8GugSt2vsNg=TnPwdRnW$L47M2tYz4 zdg-Vn)IS6y$v1X-hu50~1Z4ZLV}gKUi=&cLhzDd$Ckx}~*p7M!<@j*hnU7^Pc}&nY zJk4Xi&nfD1K@q(xb}cVZio&DQ3a`U#4d~~Ptp3XPDn%Cpl#`l#`u^-66Mya;kyaP< zb3?Qt9|dZqEZ3k=_srn+oK=k8rXsyM#lHj5{usS;rRlf>6=C3-ei|@L3tq zQk7fIO?i2F@MM7>zlhP)0UMPy?{b(Sikoc)iK2-mSNE>7+f@iQ%2NgFWz_I4JARB# zc|Fj_otO6}>>EC37+lQhI45&zluuCl1!u8kjv+g7SSBOzf`qFj&?BJtDhW5*@p-IS8 zC%U&X>tp=R5so5xu=Cb#Q9lPRO)PY5-Ad;ZF2vHEl)E!2&T%&|KjWKGjvnC zmQ^qr`fMWVpcbVuk?4DDrJy-tW}My-6xTvZ5ZK#7s)0d=?!*D%NUQqU6z9e#Ew}tg z=@_4*6-ImAsDZZ)VRP#&KB0=99Vn4-;p>K{;gB%nyIxI+@i6OglJgI+TAey4QsVnm z#%^NFP=2wX%uEVLz>=g5X|D?tp|ApIpR`t_9MQ{SYITcfAOtk>LRZJ)!S5AUSzk#9 z&CA7@H{mW;U>_~tdjs`0-F4x7u9gsANN9&*Uz|$RSOPY~WnJE*v*N01(ri9Y3v~(A z_A+`<-*lOaw1l1pZ)1Xx1nH@&{O|FSMTp(30Q;8`)$beoEz|^AJWB|Ez79YTq1gO*ip;rwJXWlHpB;ok_AqWnI zJQ%QpAD?+!Z@akV%0Fw(MbsG4zkP1I){18l@n>YDV3@~#B^>pyS{yCyccfB4-;tjF zo&9fV%I2L!xpzN5CYl0ru_h)I9H%@A($aqtspQ@*Jl~yfGcf^>&@gk$h-MG3+mV`> z7?2T8$+7gY_e0IMTTN3ZQUZeX7NZ%vK0g5eWgSNB2=Qvbl!XxM^YL8y9d>b15xb53 zTI=HWIgm~PJ|;=p{($e-*qEYu*hW84O6kaV0#5tfnK8l??gw4xe;K6Yr12IZfBh#N z*~h22e!U4sv4V|$Ll`_|3_av6OOEb?Kq)1csQlflAYV*kGA}U0(n}ASgW<53Tx|>` z1tmv3Bj}TDuER-$m)mUCx#989wlg$gj2MM@cV-Tu<&V?vp%7e0=07H46hCtW zi_IzDkT}%o9C~U1N6|Duw=9bRTA;** zm+%`-x3uVZFm&KYT2r43;H4#Mk*X0_aH6OHzEYfx%S#vwNFD=Tv4NzH^ZzNg4N$P1!3(Lmy(*Fn>%uC9#gbVLC9`?3uo9JF)F~%|9b;S zIoU=g)Sm;lEy;or5fPuLB}g#KHKJybQ)K@#pEPHW22?mUf9t;iM3zCIg(+%e^AN0L z(Ku9e^r_WVFu^NgP#x7nqEtKtV!sYbq_pE+rp^9W)ckw;86Th+jjQg@FaLaIAnbR- zzWxu9O^7gt$m+CCWH(&)M>SSU*=<7KoM{G=W)oLn za9N?CqETGC?QAjPA|i~`I|1Z?fPebcB<*+bhrCbW6x!;Vnl*$JC+?2`&IK$e=ZCAq zH^|a(eHWgBBhF>JFcMZ)bnX$~`JA?3u-`$uCVuaW2D;AJhY`N=I`6=*5mN<3^@O2t zaJ{dbmumOH>Z=H}koxsjKrRt2=Irw<7ykN=tOX`UaY z89u{okY0_gYHzP1epYLIoeDm;hlYZI8*jk4;h)5V&dXbWebg~a4p7#ficM>0>(Xm{`>(vTm*@gD(6#EQjA0* z0-xo88V&&gK@~5X*TqhFI3T_WKo1KmD}=U4GKnh<4u;;|&A_&?XgfMOT3%k>2&W~} z_ejCa48R#3kuXA(-lX*4tx|9vQ6 zESS|T#Lmu6{$t)QLos}6Zm#GPEjb0nt}|cmxD@>C+}u`cHhxU#*yp%D0>TH|qnQ#J znVHAZmKGnt$cwk{c_@WJif#+<@5j^`33;NfN0MR#6Y37`kw z6;!beq@s#6a~1!5_RR961z7)l3sB-bK~g10;{ni8bw`!Z29u{A`Xc&&9=L&j2*9M2 z==c7aSy_TWKz?9|)??1)ve9sNzv;y3-af}>F(4!$7#92toJ6}rkj~${^$@0IWE}bP z=kr|ZpebOCVuk>Et0w>oAKy20os@7roooS&6#F(5Wttv*y~7K zQW9{#BJpuxO$V&B3S^ReQ4%vUBHtRAnVYi@pGz^PW+om%>V99G=KUqjm%FH=IPOFvpa;sM+riI?n~uEgJU>L#i#nV?3HrvCu4ca$^g@nX-i2d^lX1$O9$8Ti}0ie_XSAPqZ z7$pOP&3{kXG4Cx|`TxA-f8V5?efj^~SN{HeXE5CVuBQK92}LdV-$0|^Q%2}98@KBI z$0q|`gF_o%`&Tm~Zg?U`Xv?d{2)*xp<@C2P^*GfisLwle<(#YM3uqdfNho^;?SkB$ z6qeUxBn`E`)$KR^Ty!X_iQI^Gt-pEItbDJFC4(ov+RjqgP~VqdK7@t_sZ$3}7*V*_w$#>m5{DKif_3%G9@z&3;}7#dxnQsxe)C;Y;_eD{C;J^6mhhEcWH#$izWleF1VdE0%P8ZUJpVh{Fe$=-tLsB9eLk@qdUHTR^ z&(4`U$_B3>St~$n2=I~BGf_dAkplA~!YKp0(H>D>w~taH_zshI**N$LI;`Cr+G^W& zv@X$tKZ$mi(XmZaK@Z|`UqVYbOlR8)x&Nr+)gcz3aMI8)z@cGBTk3z`Ly--~+r^a%Xbk zf5;sl|MLeG9w79HDBsEK*A`|hYrO%6F-n{gLCp4B z_`GtehwS2JRM5D7DmhwA+%*_rK-Zc#d>Obuj6gb&oVT|1>;Y3{@)HeCV#vR1w3g_u zhE;w$@8vYlp@jV!q<(i)7Vu;?xh_>>_2T5S8u$G0dwN^voA)`EY6a2byJ!UyTju2t z>-zVq%h=|5Sqx8B;G#k+2FWw=w_@?VWtIEn!I05L@N-=*Ox@{Vy~)*0LVwUUxu{?xFn z?7~*#n~lwF>m|G6`g-zUdp%4JDjDJp?J;eCI!G@8l1tA+Y0cqJYzI9q9`CZH|8*6~ zI?h6o83w|mD)>8@Of|4Gx3pxcR{-TXYq*$!?UJ%GSN`n$#RY`8RR!X5w$)dokRI4K zcGUD?rSfJ>->R{rrr$bg_9vI_OPCbL)>ntOihi}u>lF{HqKm!gz$N&Y%Btz-1X)EGOvJHHl{xYhY}YW`mW4fx^QY| z#}rV;b8k65J_frBfPR*g(DCr3YIoEm>2L$*3=9m6CcTPL^U})7TTek4(b~E?z^XWI zx-ctB&`sUhHyF2=mU*|2rzhYn zHy?$H4i9s9c<3uk;$UHF3ecE>(5&< zToM``oj}37WS}f;3VnUt`^mRWo)O+&rEaa6@1QVHgyUQcei6avdZcoqV>L*tx z4{ti7?*B+G%zcHp`~sOwHgcamnFeh8pDdh%>qH7e0h1OO!7jUyTS2yKAaWHCOiZZr ztl7O!Mksc7#nU&m`y~ahFs`!dVRGcjv;VX?AU4yTJj+4xSXu-0>_Jy^fARbu5nZUx zkRG5*fED+>Cj}>G@^uO*uX7zgrvL2BQO}Y+{VY^v(Siy{Dt9jLiQ@(U;b@s28u~nE zf6-qTmNA21Dgyo!8D+ojiy51fM}}yc<7V9}`iZ}dEq0c8kbK+ovS97oJI z<|tjd?QvPn)n>|mhtU8B`y{_T$C4ZwJTc*avR3ku zKq^#LZ%`&+d8st`bEFy!r&~U$v^en9Pu(qD!pR=CIGlt4PjBw!Goo)oPOA<@on%BEW-tb z29CJktsN1_(md2z!KeefCjnM>4Gk2fn#wkeQ*02;gUku(5PsE|Gbk_m7WBa<5+5b& z1dyg7x3J%8V8aXgIy^m3?6Mp6_>DzrI_k^Igh z$!BZms18Fc-Mwl&>vyQ@;PbENK=B++CNNjZ80Q~8|CaxFd`oD#vOKe@hAUuWzx70A zH4JQ_Bm4q3^)D*t7$8~}>mhK+En5e>fc!Yvb4t$Ji*PL}wUulqDs(yxYH_XlEdY!= z`xmsvE#4+Z%T@J^PVR}x82RPcpeJ-_*gljzh4G7(&sE4N7nw?#uPb{R2E6cK7n=<% z8phQU3eV`7-Y(Ec?TpA}dB}C?L+4C5_yxz0{qlS4JPy;1tVPzh7hju7Hfz9K>5~1i zW;EB@y3aQk>zwyQ!eFpVdUFLLzevbEm4atd&}Z|RY86F^b@I_CA$*t*EPBRzi0q9I zE~FbiO@E#kkXkS0nCTDgMHZ=)_v65_wLV-)gGw@YV48H`Un=2hq9k7{fFB%{GoCUo zxN5);y38IO^<19CaQkqwre)9h*|oOj=hVi$sq61Jl z!~|WWH6ngRvn63NTc{a@M;v=$|T*m)0%;GHcpSyg+M?)>oa z?P{2~YICHQA=67%*tpIXA~M(d(O8u30vU^222aaqX1#RMKdK6C9>qUqbtbKaKCNBf z+ANg*iOb=7UcYs7H7U~{TYuTg|Bnk0=TVze#<9J9)|kbHA-FVZ$D4ySe8spErMRlM zMYYm?e2c|;zbvNhfq4I8lau-*eB!|sE-DCN zNT#gPoO(paX!S09o7vRV-+n~;$$sC|>P*WPzzE+9LkT7_H1E-^Rzj_}#k!KN6l~ z)U|D!Iojag(lu-E$R;PRfx_g8sg4gM$=}mei>ovNeqZ^W-}Ap6u@CJRz-pZEXqmUm zPb~EF`^dqf+owa!2K6`;ZO;;cn!!&ezM=$5CW)fOi66EI^!c|80HhxiVF)BsvW(b( z{xopGhny4^X)x49XCFT0sVHOf0K)f}AXk2(7h3G7fr3p8+$9mxSTyZN>ADCF0Ynu{ z&9T^w%#z-lOC^uGa*>%X{y)Hh2xvYe7iZ;Cu82S5Z&~#ohWA|Xt5`TZq$MN;wlH*emq>RF4JzGAH`2|}H8j$lLk}rAbPYqC*YDf=JLh-SI)9$EW&w*A z=6&XQ?)T2?y6?%F=QFPW=4B}*{$oZu$<$ZuIV|nhPlR|z&fqhjR4Y>uq)n1jBf(@$-f zE5@Xe65)@qw!7RZXd{%*#33>*w-f2l9qSzf$FstzM3-E@>&#moHScW912-e+fhRek zQj>|UOUW#TX5f%I{=3bo`|Pc4bFID^jHFiHePl?@QEw3RJY+@i_(e_Xca~4!zcTE7 zjsN<@*{V@354_qE+s2lD;g_ue9OG}OJ6bT#u*Q$RqyUS zum>i$Ijn066}>Nv)o`)o_@<~v-IHG*z}A4%5llBN`ciB7gu|SF86@M&N4$$V^Zvg> z5_WBdmJlg_0HuA<4fL?Ff?!iLGqbXmm6k@(4^&rkCCK({oE(6K%A)JR2NXDwL%m1=B zwLPw<`expz;`L{r?mJl{wrjKshYwC}=QMNty=C}fj%$V@aTQ+Odnrjt`O)h#;YL2c_4_c@CKK(6d|3g7-;OpIdmL0nXd<1Q~(YjwsIWlA9|dfn|d7 ztuxWh2~Kf&&9Nwl3pvBfF2`;0cBohH?_PFBkmuh~U+b?KNyIZ( z-cLF}`k`nJ)V6a%4^SX2l62MXC^Ku_E~L3^R2myd8+c#6Q@r2ax{ms-K-_iVG9RpG zu@ZzUiyzYj+w8h0?RfV!oWA0#hasHOhk zhYXfu3)~+f$hw1v8+zaLkg`w@HlM*09t6yaeOw-fV%Tn=LmMqNbVTL*?MePjBr1IN z=uxB^x(aOjdj37lipab7jTsQS^ZV?EBlwk6;5h8fLq-OFOhtPkSSqNMipEv$^!*X! zju5Jxe9&vtZUD`9Q3`7HyW4eeU}~JY0zIT&Er_2zL^e32IQ2zxSkMHn2%bO~iACjL zGB4z^g`U%#ANNA<4Uk!XSI~YBAV(NnTUz_=gbB%Q#GlO3{2smF^VLONd>0(U3ASer zR~M82c{XO@|I^%nk)h31|j_kl+o0<;ZuOi^fte;d$Y@Wq$~Yz zGmPAs0N?vOF?+uf|Mrw}#tZGOjJRxyAFJX2i+4Mazkq=77^qZWVPSbJ@-;|_iHWJJ z6IPhNdj*&qO(77i3Q)fQ;ag4zVFq$s*7_dn4LxtKYpE+f`HI7`a@&NL1WCnXTD^+S zGS7HI!}DH6u@s1K8_`d4(v@4Vr)`fe;)?cl4w|ebL9S!68tfrww^#=qcjABBIGcE% zFUfDoOQx4@O?6kL42XX(kKEDdvni@O_HXmS*A#jeQ;{w9ix75>5iC*?B>=EDKTZB& zZz@Ow2*brBWu2#rTscX}w$nMQ^S<4lBZJCq&Mv)}d?(=s^YSyn^r^!N0Xq<#V$- z+xp{^Iy8fU?_*}4^ojHKDZlOP{yYAZ8EbFa$q$uWDhmXtH=r)m&=%cz@B|uTR-@n= zmoDOO^btATYs=9m{4EKK`=`tMP`S2W0F5@ZZ%Y_@?%86H4>>NUc*W+3Ul(}_=*zNI7UA?h{*>o|ik5G8;W%UCPABPP#Mx@`mHNHk@Y&~KZn`7?L_w-p z$JZ+J;Q$bWkskF7mTj>QEdLX@FC)m^H5CJcsAVCK4~9T}Xui8Mk{|p^A8pCM=b)HH!EM*6TLqX zewjr2TM0WOmA>#<+C6^HbL{6dz>|22OO|VM#)&w!TC25UXo`Ij4jlflN`pA=_G*ZJS4#BCBb0V0%p8ASKk&p%tibIj& z;DzS5cXZrsW5q7`n>$MdGt=MVl!hbOKKmw6>wOu-72B!({n?KKwb-)5G#q1vm*OFA zI{PzQbxB?a!6>y|)s{*X*Dmh9T3Pk$u%25D)Gu*52J_>-T6~6{UD~D-Fcba3>SjvL z=#?{f@1wB4tG!Nj;m!&4w&v3IRmjNEY}3Ren}rBS1aZncl6>|%lPwrk7x^-uA+fpa zrn=_rNlX9IQL3L9`mU|+Z1%S|9RYl#MG`nVDgwiU&ObgVrA8mY=HiIp+nvLq`I72M zJ;J?gq{fHBi`IIALC)3`zaw6=8TXN-!WET)d7}u%Obzd(n*5R9AayNdUYbd}u&CDFVncX-68fCBDZhorb2L|=N#sz$Za!t9|yq-_RrKNH*C)7gg z6u*AI7KBpxwUZT79Go|62(oZ-1A`6Vre*UQC4E2YtQNdew;eCMJUm*as{rO`%n#*v z`keuaP1)ZoL)^jNB6q)h+pYIJo)JFB69Nm2a;>}Kv%ljt6u)sm)*Fnx66PG~XJ(`D9lK0)rS9{1S-_9~Pd0#r( zG;lNNP!F?RhE+r6=VeJgQIil8#{isn2J^o2jn|_IY8CrO*V&)y^JqxW;@4?$0eAAF z@>zZI81G<6Sob)3pd!VK@V2qKak#iJ@5^^r%`Lu$o~FmNKZ|Xvz>BT?aEp>c9@g^}0H=Z3#b1Y#7@awh#Q?;Lp@!eD=zg z?z@|hei3=hPulLyx-{;UxjF6mILVbg+A=|@xY4SHB><-81sH^4j~%qS5Y0Eop6MBd z&||5Vek~w9QM!Nc;XO3OXYtbH;_y4(agz&IJc=dCo}=k1(rF-m9B7P@zhm>FPq1X5 z2rTq2di9q0>)BivqmiJ5*#=+{s&}29_@91XR^nZR#u#3#jcnDB@L+iX!Z5MKMK>83-{?NuU*!^3Z>L5zgn} zReiS^pKO)8)N69GJit-4@Z)u)m*dToA#Qe}i^hI^4 zc`MiocJ%|h_i$f=3{G!64~)~DdHVw`uM?h!74kkC=t(j6r##tR&kJhS|4H0C)pSu& zlJ{g$hDy)Qm87MWneT=kx!U7ID`hI^ZdT6sGA;BpjZ|BiP}|wmH-|Q3duQ|};x9vs z)aD2%O#ef*yiLYL1sqomVpl58UST#I-52pS3Q_Xg7&~%l0Eo`*)5gE|o=a3l0XhDU zH^m2+=YbY@*BHq_i-Ms~d5&`~y~hhmSm@c}Gk&L#r*e6!j@>!0ZcG)Rhyji<-SohT zJ*!y(eA?iI;cl3YB3R2<&?o_AR9QdJO_ zkHVByt434nMu!_$q?{?16VmT#>00X^G#X$jWSJkg8A;BV7T>M!=4=VC>2+sp>;`r)AQP)o$%SoYI{#%s4wA9Cns}8F z(rjMF@+M`e;lvq}6BOgp2A(h5o_$gqOE71=G*{Z?X^w`0sUZm7MO3YiioK;zGI}4? z3HJN(@a5f}=MG1EE-^s9SMIb+r<{!I5AP6hIm^Dc(Cv+eL9oz&aT??1u@Nqy(rqgq z8mKD25OpvyS5hdOqu$DJ0Gyt&%-#U^EwX~w9LciZXHx7}AxJ_qy~{bb>?;W!u)kHC z*odL@Y|cZxGDs%==ovNxo~ewUnlsHFjY<)`DVIdgF0Mj)HM-M;7#%VvlX@wRp|YBA|i zqR~vA7>)LvG~cJ`G!!(h1<**16>ICYmM-Acv^S(d=l-5f##x(r#RGcH*3iIrG11(8 zbTgvK#lvvXf*Kr)&KMS;1#CcX@P(qg=^1Uut!v;7$6>hNWWu8U$|M7~;ZH}EXlsz- ze_LArIr?<}bewM?oyL|nxskp==QcdN1Hwl6P3J+wrSWl#nZmw}-Ge+$Z0w&&`m1Fb z`NYTk=-FxtiP)x)5$TR?$v>@hBs3Tbi00(JySM4p-vq@1e81XV?~yl56Y&XPcO`Ad z;g}GiJF@UN@3Q9o2(P$Tap-n}viW406mV$0u#ieH0``ZT=(F*MoY)!J!2QZ*4O6w) ztjXxW+9K1{sOfRTZ3^T`-Pap@Thl-EJ;l%`TWG(X+7_hh?98o?XxB~amX-X~Q0IJg zM-Q{ZWe0Dh(?_79pM{&&m1fgWr9dOG>s9~zNISH0+k9s&mqj zoo)#Lk?|I2QXqVkkamX=k~^{_gL&NR9|7YW5*50{TS?BC+gE+PijgQWEQRi?d{2|X zO+oY7eGx8j;&8x=M?-!{YoGF66@v4g1WVb@`sEvs7ME)_bI-zjy1R0q`Uwg;stUNg zMZ!W-4FK<6%L4FEollKOM9&@$mfd{_&F-HcA6?pl`O*xfX}$)!rha*?s_|3>SN;3| zad2^d-m+1+9p>fbWx+A${TxpSK)un#yT*QB;WYycI!C)`=E&!2s0^6zEADlR(N#0- z;Gm{vYwGK9H)=KIFyWAtv=1?L59_Oaw-*kFo?&eAyO~1H)nOVvY*9j464|4M z!do>hII~;+cgHbT%Wa4in%l&95X|9bKN^j?@C3=;h>|o|7~hifmiWHU>z1b~s;sxS zy&~l2s&rZ3c}sJBb5*c!G~9mEF150(eN^|@$fmGP)s&VtU!Gq?XSZ%-?78l|Kh5wk zI+2(BA|fIQ5J;Ln z7Jwww*Yomji?b5T(Hl~+Jtv-PG_9dhBaxh(nvn#Bv2y!djb$=yJ>-IcjLGooK z@iQaP4q!y;<-$mr^dzO#!l+i@(pd89xJ~>4?tUAotF!R$Xfl5 zVy#s4Fc-si0^no|7xnY=Cb|I^cM2C}e|x!x(q|?qiND3Aml|S8jYNysXeK#>r13Ee zsyF8lqrzhdFB^^(i$&-nGGR4vSBI#~634<0iR4iINnj-?Hn96 zI@8q=4%@*EgSje^VuZ6jLx9e^Zc6UVC{1Lw@E@e>J`bpC70;n3fS3rXBj2!yvZtky zXOdFuuh=dn!VMQFiIT_rD&viKsqEwAKo%Au8oZ=_0&|1ToMA!VWS9)Gq3W#Zi3Kam z8Qj*LdG|SKZBS!XdZ8;SkM{Fmmv!z9qAi7Bci!o00%NcyL@Mk~_#o?c*0}y6GO+rW zP6oqsf82N6oF0;1uW?vQH*?5GNiSxD1!)CmZG9|qZT4pJJ`<6s2$?hz%DCIML)vOC zzeVQ%l|vm4g9u);>iL^bzVt)A*_Gp~KNuHcy*^-+3@PMs71sc=mD{6&xroE?=MT2SJAn01G~*aejnO+P`h(l&z;S*P|Ks)I`q zY-*tm2*59D(LQIu8@YG=?QbFMFS%X|Hbs$p8ydilG4T{INvVmK)UZu7C(Fw1yhC?p zFZh=E5FFci?3MhiaFViC2UxcupNRW`Ky6g$PPkD8lMR#3cPEp$3KNAjt)m40K{Ea1 zDxOMvf{B7$QR)qaVu3l=P_~xV?e|sV@4?jUG1?;c;T`o4OY2Fzt!bt1+LwFSM#%jO z+pXH~CtN>lf#?4VbcCcJ(XhdF`1w9_GzG!~d6#BA|%7YMk-b&@tVTnfX%#?HY{M^iCb^8^zf3?>XM~C{2+T00161-WonSFN0Qf_-q zWKT#<#V)mWC=?gc;cWu&yUlhNl~@=%n;_k3oE?^6?y|M}sVEUK>+ip)K(5F#e%>#c zTJyUCUTvRc)xWEQ0=oT9E>Q3HuWYXOA9z@J`TJ3~H7ujiYkyJ66-ap6Z9rJId+h`# zXQxf77BWix5f{@gq57^@>xLl{@a}u$sN|TZ9C6x4+OqIeLV@-4*JmF>chFPU-0}UTPVy%g#DQV=UfW_YttFyiJfUauX4JTdX=HPktkx$#|3~&Q+ z(G}D}wfkR(kw%H1Is8kIRqaQ<`bU*Z)fe{ndMT&zbz1iwMrW%B{yxgrDpC2n-`lQ( zM;gY>VP5&lsl~4vAlz`f4bq8uIQ#wHIyF08P6A4+e5pPB;SiHMb07@s17c3kzhyua zo#LK85@Rs(%M+?%ybWf1h^K8gUHgnJ6BktkaR?h#>&}TwX7l;ko5xr4Q@(upzLgc< zLNh(s{O@%p2)IJHW+t(=d)rSk6`Iv^JClgaYReyc!!S zk|_z6P+&IV7xc8JEm!wuGSzyP^p&Dd?YvkX%-kZH9=`Mii&uz(hYGxL%%^LU=Lnmn z69SFI{%K~^F%qg|^@i}K6%*(T#>B*=S{!fG01OsoP8b{>me<^Ta+4DqF*=I=hBhRw zZI_*TklE*hr`p35`=|%E zt+^rdUr*(hA>Urrxz1=5tKB50(c-^YX%x2E)2`D?0=06-v1U=-?SylhbRG)7OnmX+ zV|ZJRNr1M3{2Chgt*}CN%Rfl_ZR5plAd7j==N03u>h78nvW;; zgsO=ZqBpZ1i9H?}!tj2`q!bCjZ#y50kXDTR81 z`^?m9Cy%k9%;%w6sp;=;F`sF#N&}i+n+_z(`DIXZ2JFYeF0DOx#$fhszDrTX)tLl( zOz|ST$RFi=i1iR_s{36(&8O>OOe}qU3Y!j_C(p(zW7-XG4o3&eAG`M&#SEji^<0cH zs073G2-vWp49Lk5*wL%}L${2Qt!$|MTmx+tEc7A$m>UPKO_&=>M_b(Ax_&d6F2`qh zde*hmbeljBPuk9oI$4>=LC%SX&D90quS7rh^hsh&aa^U_aNCfN=ensgJ0e_BTl1Kk zIRASQn&cPzig#hxHKrJ+cgLfr71o>?r|q?WIg^!0uHffjG%2N7BoRkPt)EJfO$b#O z`+n)FnVi?1)sg|s(($-7ztJGs|GBIwBPoG~Ai|)=BIWJjPZi{jz#il~y)Nn&btV2&*PEUssQ)FdQcl z`=MqrDgz6vx!F=e-nRek4@ZEz@s(3IYO6foHpyHEGF|Bwcq(V5#eH)(MuG&5|F>>I=#lko(*w^h8((W1(5 z-cmhM+v3L#IUCRlzrXs(=}k&WRqHh~J#T?Ma$7^=eiwmERV-C!)kig897Jm^ev+lk zKaOmJVyxnPREQW@bMz94U;TYD#xnOS-kX>)LzJnj=4;$ewFP$dq=Jz!e>Mc^|L$9D zZSA>{GsmL?I0wLSCS4e59vB;A%mK#tZQQW^M-~bTvu}eFRKoosuVGCh`9B9X5&{s)|y8a!UKTNxsE>$*=h9hfE;g z34e68`;wXWi>hx3^fw_1`$iJuZp6spl2(B!4(r$e>EJXc1+{kkAbSaP#2W zCIiZ=4Y__s?Oi>SAER+WaE$u`v_Z>_@`A;I=qbs6@a|w^M`>&5moeKndgGxtfeuZh zi;b-++vkgJ?-Z280Gp?gMr}GYg^0s4pa@zcN1 zyW`0t$?qc}i?-N|cnV5dIQ$a7&rjBG_F4{o?$Bylht>y+2v~_mclqKXW4;CSSmiF~ zMuxw>MSh7}4xY@)grB_B(~4C={A%` z6V6EImUMz412OKz@Ni|Dx!oblRMpeSq>)I0HOl|e2fPe7$zPf8G11y7zYvsx)XpP%Ta&|8-`;J08=It^ zTAsBiC_(eg3@LfTS0UZxBoRI%7;h$~XcyNP>~nYgNI?Cl`SnQ!CR+dGe(H#l zT3zz~nBqzq=6`?w?^ltomw#AhU$U=;-K@Ho{|SA+OktY;KPfyNq5tP}ut*Q| z|KBfnyxW;x42J%@T1Qzt9CrYYWo)eEqvE%koEb{h{^{WQ^OmE#MV@ZE*4g22FIru& z`FY*hdN5R|5L!Ismmxy4zGuedQYaNB@onu8`=1XU>5;tl_=yWXf(7Amw!J{70qhiJ z$8Nu6{Sn)gjx9FrAZhulL=kl!vXz^Msxo zZda`5G!{KTa;K^~A+Y584|(Vd9uD^hV`Flh`qpD#B0M0_{@s%{skZSg2JTMz7PcFC zX`JGyn|gEYJ2bF*K0npTt#bgCa|7`Ra=m?{DYCRoiY9vCV4@OS!#yq5`R)TcyrgF3 z4ve8!VHMV7e)twHx>(UbS#m(1Q}$HCwdlO@!pV4sOie8$D3n$sb~8DX0E1E6NOu3_ z1;V^Z-Q#!ThI3l8t;%|77R8&K1%_@U?BTLUj(aTqmEVUp^nW%*k>Jm!uoNA79)sZ% z(Hl1gWBytfSq>~&1}+DUe*di0=CJa9dvWl=N3Y4;eai95%;WYrB$!R#OLMr=^=EB% zgr11@8eASCo{({{y{JRTn14RD@l$bGmREB-mPo)KJ9sxY-N|qANU_$TdHaq2yEo21>-!>EBb>fddZ zq^L z`IzhOT%2v(Siwyy?+nXEO2-psNrA5N7y~5(reXzoLFY5~j`@D>VV8~xZ8|oZ&muOu z1K6`jzp)dPJjUOJ)5~*(#$TsTO~ge^Uj`tlX&dMez>q5Y_Wc`fs*xz)ow@i zX_#|td|Y1%klDF(+q_EJ2Pe2v6dz23b>eaZ7^7512CySAlbYTyI#$}*_@ybo^|+4R z3?RzLIQSmzy=DgS(uud)deutr zvpiadF?pQUvu}ZZIq2;q7sGPO{UF0d9xFD7J6*kr&(q{~Cb9#_I6970eB|{u z5s1ev-D{TzcHK7c$>?H>lQuPuz|Zh#ay$*E!9iq67IbA8PndsL%yPe}MI>3%jo3fb zj^}6YLgC1Qyko#+^E1Qt94BiZ`7+4iT5`P*0*2N7X=7)?>Af&%W@1Gp97H>3);7lF zVed`2vfIYxMy{%{k5N21#tRAVCI6zq?zN zTceO8W3z3TQ0m^|NQPvZ7(+Nl0$VzT07t`^C%)e@Ran2Zi%(^fiB_?uMP)J3dZbFY zXjay^7U2>E9?#7w#0sLBaML-H3vQbIS9M3~qkBZ!X4%+QDu{Uf#9*;uW484!Mzql1o;##}}ZrBH-WNoo{qn6d}%ER_^h=Wf~{v@FK-)ykF+ zex~V^)bl>kx>CDsdGXfU$=hhr9{6=#bHAlqbyBLLO@42!eU`ay$xRhYuSflZqg*$M z5K^jlvZ(F=GVW>jFyMfdb!fQ>+2xtHSsZRpa@7|Wx>Tw`VJf~vX7bji9Ql=6Cf7+lbEII{pmuPBF?IgG~6>x1J1?PC3UC-X=*%!kH2%SE^AL_FR0etjlt_mxUQB*I>txAsBxF6!5BDj@z2UT4v)V{3|VA%c!f zuWF(6hugB2Wn;RW{a6%5=|#`I^RlQYhkN6Vm{(1%285$%#=oldWJ`}r!}&_OJ53bk z2Q!dP57iwKyi9a!X{|a$lK1BKUIa#;4ho$y3(4(4XAhkydmX@h0u-Zl-f8ngS!cR0 z85X;f^3G?f>YxT}90Eh$^I@%0LYddq-}XJV(vTTZZqHft+v}v?uKq=(DO8p4>Bl(R zs)};1g@fBG+gqB8Rw*}L`>Ex@)R(@`K4?CuHrN|+U#V)rE>@rC*Kng=zPonodih)M zt^%s%a*I*uDcmVfEF`2UdOP8~(UiL+a=PYlvA|A_7xeOzZ2Iz=58$OpxCsZXLv`h6 z*b3}|5PZWiMk2YelvPJTEkAW|QFCEzOXvEytL~Z2xzVAm&sO5gvAH~>2C3f_Gd*>7 zzYh_5WD9IU;dKv9>}+=NsIg6$!yqH>c;N6j!r$@*iz{}B+MJa1zvcR$)Y4Y(zX2B5 zX(Af8X(uxNd*C5@$i&`lAETL}XipC1#`-sINhbQ`fI~p#Ea*({XX_Sg|NIVgg~CFP zPx;SkI&prHPWUVq%LQV$p4HL2;2rBt#l|E-e3yF){BK-ypWaoX+lLFcAU%{j->gfw zC&pU!5`Dfp>*j)gWByloqpsA`vPSd=}_GYkDiT>EGe@l5)?WiN|ReZ$b$gm$A zX~e!O*N&Q;B9LmAEvduy-901g`MDereR2|I{q68h+0$ou^Aw{}BFt286kVe0L{XpADWPJvZy4g@0aex~A++9= zN199cqWC*v_sLNLl?_jG9iKME59WbW?%eqaJ!pdtV53F3Qw_c>hS%~!y_LW2@+{03 z0YukiUPZ)whq?Q`h25L|rayW7l1wfJ5G_9NNIvu7xfUb(bja|U^zEe1tnq3N<)>_8 zV3&nH4uHpPyxSF%KN=Q_y5V#hadpRp?;9#r_ADo;*Avw?)j3#l*H}u~7V*%6)~ke# zmQi2~)N;^YvQb!yQCQ}%;%=EE+Y*+d$usiy3pq*>M5u~fVSY(>vnd-Oa$5dnPK%9z zJoQ?;&(@~^=YkY4;pUz`;Hd|4oNa=?!Ru&XsrAUCOrNQxPnSr`f7#*Z9WP_>4u&!+ zRC|XCB;YL_348@Zw(X}#I^K|);%-&A_2td$%Z;QMvy`9dAR~nBt+Cvbp zrE2wgnf6I_>QnPJ0zYD;s;05qvVO>O-p_C8h|KM7JDgJu8WQF|V&i)fVy*et&4Q;e zPJn~J-nPLb=%-t9-zB)o4QbE3r8ynBUGDwdXx`0 zE$hY_g(GWA8ab_w)=HiMbnh#SZo9o*y5lQ25IfZdcU^cPaf4)7yeqEM{z4nDOfL=f zsS6iFOmf0|SVD2z-RqU|(%aJ|_=_ z*m1t;^N{gB7kAe-)6{H)S#}q6+o44Sgyx9TLsk+=S&6FN4G@h{Qw={}^Ko>*Znzz( zC~)VSxthA}9;v80?TMpvFi7rTAdY|k>d?3FS4~wrfit--{u0ARYr@1X&RA%2X|Nciz4E{vo?WH%n{JH}ivPdVzj_2d!&yG@9yr?HP z9Bo+0V9S`TzG1hI?K3`*)L=p@Z{I4VhVveO-a%9#$HJ zTobj2)xvR zI^UO@_}N3~MH|@UJx=$g86k>WTz_SJ5f)aTALu29q%U&+enn5T?A2B(*wdCPtaO;f z_i8Tp&Oep1Z>-661q6#bdiH+BR2?=FwJ}LNK;MN*j5HrYwlMZ(fNVFavQ^m^K7y=+ zu4nYPvRVW$!b(+n9;HB4V{`0}_ZI_InQTk_dhl%=C9XX<(CGoqU_$`)uswEgoS9 z26yuQd744R+;NbImhaKS)3gl4UN^*(?e=0RJtoLE$&Yk=MOACCQ;(S5nGY*W+XE#A z%U2nJ`r=IpN5t6+i>@6dB`f_%8B#U#Nw1?1{vh zwIJmm!+-EEQTBuc{yS^W4Q;?F9wif3xLZsUet~-fA=RXfHkYXL-*+8?G$#Io6P-dFc=z z>baG{-RzQ0QREn!Vod{!>2Ig{jbhi(yymB_!OdJ8(ekW0O-sooY-bqR=?tb+XATg zjo4#>^a=-yry$j;@lP&1)@-7;**fny#U?YZ7?_yfG7a9X+5A_Pc$xTW6b>kYm?lom zrdppr@ySC}S5ecl&9-g>9ÒSH8jAuPoT5xQOjUtomB(r)!!7>o#(F0UPw7D11g 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

d<(5R zfWVOdlj6ccMrP*kqy)cP|L^9mW|0CioiDKo39Gu5x)3!%&>Kz4Vpe!#OS?RU&}AnL z+iz0(2jRF2Kj=JvQXl8NpJuE~QCs3aO)aO#9e&WCc+3fmCde9AzBiyZq0BpJrya>n z>tDdVoNT58KEFlph!J=N7od@mxnigOz#f@LH4H;K9`6 zi*>$HLOPOr(({+)z+k;r#Mk`~TGV3;ydcOa=U|-=&E1F=rVyd$^${YbsB!UFXhu{i zm^0I>7B=tdIrLd1eHu6`B+&xukBP&;I>Pmb5;HVYF7~8dAd%&wu({$nbG<^n)54d0 zOIE1I3&~I!_gL=D`2gZ65!kOBd6f6RnG^olABP?RtB}p>=y9tW@(3x8WP{%>Jzk4= zL{#yLpNBuG5du1RLm+@WQB5I$=^kgAY&a0irsFFe8hT!6oV zT_KcO$aq57elJ~0N6%_xg2kBU=NREuO^P$6tvbapSiet<^J;|f77nA2>i8*`#GcmU zBPayd%`HFqYZ-KIM3bKAzWI%?9V4!dv{&t0{8}N%t@Qd>|{O L2r85K`04)vaX67( literal 0 HcmV?d00001 diff --git a/packages/vscode-ide-companion/resources/deepcoding_icon.png b/packages/vscode-ide-companion/resources/deepcoding_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7268ce7e8928fc179bbd2df71d29769e8988af13 GIT binary patch literal 77727 zcmeFYXH-*J-#2`cP^1VdihwjBfJhI$qlj>jqVyt70!WW^qy-sqY?MSmir`2T=?DT! z4Z|P=1(YHkqJWOnh|){mote3=`(E#Qo_k$i-Y@UEXU&Z9WS_nN<@fJ9w=Y^4A7mF` zhal)6Y;w*Df|$TZCg=bQ_#;6`CJFq(hBI*tg&+{ZPb&2ffaSA9*c zhj>|Ex3KZN?(eCI71P%f)rryq8w7fVd5A^@1_Xs_Md^zDwXYWV%y_IICi>TsFn?XK zGmHa89n3F^8U}}WiK@vf%6Td(s)?#=%40pQcz9m9A}fkf#3(5!swgO_$SEpnDQjw} zsEGdaj~F;E1naG3b?*E>hk?K8id_v0!)YlfL`Fu+M=Hw)hxjNcX=-XJC}I>a7&)*) zE;KqQ%p*!JC{+AkJDl?h^$hXFh4}^ti86NdxDp&5rYioBb6zu(1$hXnk^Db`cLE5IudEDHsMDgFB>+|}T);LxkV{}(>~{q?`O!D$(WczJ{c zhu8!M2mDJ@7yq@3C z*yGlB?00uSHR8-_V)G-=zDoUE38fxB(s;cULa|w2{4DkiV;}P)R zcV$q81v@HfU_CGz>WXq$4UDFoil@4!oQ8(x6*+bFD~jGqYMx#i8s1`}SWhkQ;E+HM zfE3?A4<9cDT#%2L=zmdT7#t8BVjheIGFSfZ&tXHuiy^_@z5(EiP%GoJqOg&nnv$lP znjA)6i2-~-1|ZPHHxy_&`X5uW_PX}ZrvP8kzwoK$;mI%yT`^Av9=xz(|2+2nKZyB1 zW9aW~Bd>aaP5%d!{JktR*gGuJBgE^B4^YW}fJ24<8~LFg5&tvs{~1R9KPCPj(RyC> z2=eg)j#fd8!HxpMBmJ!yh5xtC{`&5}d9Qyh2QHEE@!!rE{N>;7+$#ud9Ri&7&9K|< z5JK7wUgFihMThTJzDYwY2Bxz<^7B3Gxn-{SfofDUMVF(WIs72#B$kphbr8e|;t74jC{Wkhm28cbor}&Hv)& z|2M|qUb4*ct|LR_&*4cXn2_^Cee=(rO9WE#tNEnnN%}?9OEF;ZR>o_57MrDRsE2K$ zz#-(YG8~0m%KRVAeJL^^ z^;1T~$2(f~m)Cm7320vhe&BkvbaMj$Ls_yRe%AorTjIado>!{faJL7#4%ainjPwZ}uc6)}-#Yo_)9tok?=K5-+ z`;2PkFQvM$>I0W2%iuxcFAVA9|lsa)OjN@~@= zw56f+cDS29P#RkSShlR0`J;JzZNF>yEjB@!#z*U-&Uj|@iIm6gquGuIRPH*@ zP0K8u3Is6Vc*pROKa`y_3Lcp|yuGt0;I4~1gyXA1XoS$V^pXm4#bkbZ8MT(JR`ToE z^)C*@c$%a4-Y4>J*c+`!9L()jHl$>>cZkRK9!bl5E|gg}x0>vKFWx%i=gm#9=}8Mj zSwh?>?sT)nA`=|-+7Z52tgn)o*oCD=QHA5DbdH@NkTfw2=`41$huRjDb*~s({ot11 z*+Ws-=?Ls<0^0GLe%hA#lrhKZ8vnD6`ZH_T8>_pv9EW*%IO}xs{t*8JfMtlT~sP{TsbeNv%7ua0e=#K}>3M@()oTM}>lQe(aw$;$`uEX#JbgIH#bXOHE6b^9ovY#aVV!do)_AA0U5jjo4QqBFMpkgf)HJ8$#% z#YdGMB}DS*NYQ(2!>vvLY|{7AGOq{!cD1676 zhOSLPK(N*LJ&UG~MDZC5snXpOU9|HRNoPQabAbrFO0?{1hVK~z~s;r7d_ zhzPkg81H_EDOVoZD{0K>YJ3A>kE%ym5=7hn^gYVLf`)4unU-IUuw&0OtHy1!I{A}3 zJKUn34VvHVg}Phl(jmXhwxG4CW>g_GAI+TF2ms;jt}QHm3idm|;P}}?$S(aT{Wzp> zvUI;Qt$Tl}#wN>!Iq#W??j1vzJdX^yL*~W@L#>eT$u(wEh~bipdAXjUVe=|(92YR$ zT*1jm9hfdsTjRHHB~&szBS0t)5!1Z=S%O??QjRaBq~GZK`2IXhU?hB`Z8M~CI{W@t z#8+-%*2OO?np^fno61f-X(k7DR$()Xq9}gcK?=*}VH%H>M%6>UU0s;Kn*p@jf8~U( zOdK=3pkH9K+WJdgt84jZ{iKYYAwAtABPGu^Ez<%rXA*}I`YbJao0}J^UEFv2)vO7F zwhZ@hx7;k*4$(=lp&v(8qWPg3q3N%)Zh>_*vKhxn&*y$5j^iuS+<{^m;SUDj!25q4 z=*%p5UUJ_anT22_%nXv`ow)MMFdnWKEVwphadJ2j&GQ!tBxWCA;V;ulj`iB7b^rMl z5sY*_w|=g&yvWz8@TUnb02S?a*~||WdjwdszBiKzf%hR}^Rv>veQ)8Dt;eBYw9$SF z%g6NQy{9HQ9lIo^cXq@jrCX2>G>o7dq#SNKF=AyuqtaNa_iAT(p_@ButdR6Y325Ev zR2)?_*M@AOC_Nukf4BiorWe(Y`t5084!YQ!VP+9ejr;fdn5NFy38kX5p#e^N z!M6GReyh_fJzo)>>G$$lnOfKQ{$03N(Kb2KN#eyI>% zu45_Mp+jfcM9{82x(|{sT>zXxU0uuveMFS5=G015vhRL#dMaV8TyQuV(j1zRrHYfK z@G-P519+a3u~<&mU4%ME-hrKHCzDos(uNY?nWqBpoKbm(DV#dYn}=wgASda2t!hAc zlg6MWg{^v??mrvVIzWkYf3_M2PXd3*OW6D2NDYS7@Mj=cz!R9VZ>();Y$Zbeq|v@B z+{?}?MYqZ-7gse%k!Wk|K@hO<1 zFW)mZk2)n2_SPhPWtjM2%DSt2N-gAF0WkqCJil`@oIH*fPt-@YumxZpl>fK~!G6*V z`NexFw%jq>Ot@>arH}taj%Sym6TSp#;*>Pys476%a@a_f?ybRB>WOHZhk%8Ubw1M59nZ&kNGW!JHv)L1sS01{A@zJEIT*-U) zfkONc3`&KE;_BbxL-Et-oxKh5oBP{Qe-w~zDhrtDh>VD69Nj#HlVd@ki&YtPYCnMY zI2~^{LYtlB+^!P3+L5EUIk)~R+Ps^A)v8p>#1&K@S4%xQ5nrmOu~D(y&CQpUMq)@@ z)h(RVC_)p%Y$Y7a{u$hUnoh|47*EcnMBwQhn`*@z)BtCK03$nAbLl5bQ;I%RKqNi2OJU!& zpFKxtMJ$)q8WBj3{$6PJA%!KLV^fuim0gu8j<#TlKkmS~D}m$TWqCk)!w_0e1cqjb zuTN{PBMjPfLMzN4Q6k8SHU>p^5I|U&)mPLId91Ne6spSdejtJLS_>I<;#8z}5g!L-%~BhlW7pG4RS8Wldq-cOy4a>OlH)kfGeTZR{%j3e^pvAi-Qy79`<&`N0wtN7bNm z17NZag7@YzfT)*0OJwK@h<#?9QhNq{Dj)$SIF&FVyz!oi)2Jzwpo8;Wba5fWZ2Dzn z=*NkL`|?s9)t9%jR4Po}YXVp|A)1Bzbb|mIc3~ho)75c{u1n3ph6f<+_a*Mv`w`!^*4;kzfvUR9k0w^PG-wY<9m7&GdtV7d6__nna9p^30!;(4 zDUxVUync#6GP=Up$TEAv67oq$EYAT@5eB`6e#aN3&0KWrp$!7>gk)rFcMFYKYI(@f zp!i{KFWg7N?JXfWSa=WdO-88Dv|kK&%#^_Es#;Dz8Zt4I@o?-kT$3$|z_U@s+?8pv zG}|MBr_LbZepd!K?-mM%t~rx)alptMF_bFTZV9p^6{Z?%Y-N#{|6{p4=(`Aa{ps7)D$&Mf>>oOFw?h0RUf&1+eP#QG= zLH$u0>`@{#Q+Hk91$-+(?LulEmNMG!l`x{qlS9iSe1FS>c9oAXIkeG!wT1h2Mx#yp zr*a`+Vcb#tY%K2v$HZU(1s_B!$K`jp-FXc=!WW7)2n2l@gLH9tjGjRja6jP<;Bq#G z4W~d8rE%!+8xX$q0D6~R>lJS%NJv}NvW!@4+ig5I?f*Ro>B=?1rh}X?+;&O=-rx9{ z1>C=zLD@S^4WTxJl6O@<5kSOA_7*Z&EO9ul>yK$lE4n8gm~c)3=5+zm^+3}to81Dw z*q(S>0`IUiX{Gt7gG;bAy4Xb!95AtVI(5cmjP6DH(~Wz_26kfCrN&Y>;A;juu5gaH zwi%q0JFhp8lsJv{eJ&$2orRXWFHL18DdJS zLNq@Rf%NNm0auLtYOGmU(a>s70Xfx+>wpP11>*QfltRaKU|)=a*mu~}^c&OY20e(X z<6d3I3lr`LBc5Yadi#j3d;?9$wmj<)@2!UAfN^VIYG!CB?b z1QJ4-vCTHQg?x;>V}dY%t!jRn$l)WQTlZpJ2r7&oZA9RaR}esm2HSyGPtb5rC?w6k ziE(%A2}~!QXF#(t;t1_Ll`~let~do-DL*o|@Rg3MLpZ}Fj2x)XR&sNPFho+vl*up6 zx)E}GlLzY?@{Tf3)PDr_hGMuLp$xWtcFeD_Qp-D8q`HTQk5E>+-b5inqL=~5rRSu( z`Rq*yjiS0t6T%nI8y<%?OsFo{6It2L|ByI{T1U8|nwWNA*pKMPJIs-&K2r=4eokvt zWsrpAq3?=v5m0vbsH&=fLHXLI6l z1<8V8#qGy_+rtklUzVFk5yMqY2``832m_a0VGI(uuXz=q$Oni|7^;rd8ojG+cu8EM zcyApw#d7&w%1xw1aj4*gsTIIJ3r>KGOTu>|?+gZE@kSPch16_8BfS{0j&e2az3=K6 z>I@n`!OU#X1ajh_j!SSTx>y(he!jZ8KY^EK7ol?+6!oqtkgHK$%rWm+c>X9jxncPL z0Q6beO(}S0Ie>bffowNn!XO(!NBC$6RRU}D+-5H__&NH1>k=DGC^A6{ z#5=G6QIw|qTWZN`IRfcvBp(H-al*k4aN$b<=09lUpE!&LqSV6< zbR#Qc#MZoK292Uw%I(L)B#F3EBi8*UWrbjhX68ll64E6Gg1fZ7TvAegCACkS1bzcFL- zskNQbt)G!fAO*>6BpoDQFfoT#4inxGfEmvO5f=9n_^X<})PWX2k=CewD$~8Jp+R2Q z$p$Fue(C(3-j3NaAD9p^$8-eJc)px$(tiaQk8hqiq=1g0PYy`KV~^OVVbmk;whX&t z)T8~c=25pG1*AmWwfF;9yWz2v>)MN;*=$ z%}~uj=WxLUF-q);8U^t6WMJUhOMxf}_|q>x!+8-8&^@-PW&5q18czV5{Wn0ib zUr5=fC;10~Xy5!XO&pOb0<`*~hLl;uLsSU@^)Ov(aPkmrRl3&IJR1-aeC-6D^buH{ zZ=PA&gB}@tA)oL!(09#uKr{(x_|sl*9EHoh0q+`)h9&z2ChNOd;*Zeq7>_lHj~ea3 zd9k05AvVDW@cv2c(=TZP|6?igvtm#jZg@$zLx6o{D?6NumQ3veKdh594%< z@@AB*=3#eLd^m)zWwbH^BQe^`d+HL_pdRUdxOFJx^Sd)%tHFl=RSc8!1`(=Ss)jqD zvR)Gm-87B>bM+DeuYugb>O{s+W*mM1U&!jV@JS8iFQ2uJMln|f_bjE6qJcV!?{A~$ z=wVcJGBG|CdVrRz+cWtxdL~@`9O27b0E9XxmZk$5g@9OsRv~Fu1eq5y$GRPaN*g>4 zU{pKgWVO4}#qcfj_xRm&0?srZ)p!NGf4>ex;ODK$*FX+c(PBc#?B>+Gv2I0G!~=~K z&{P0<3_pEBPO4K8)OqxAhKnv1mxK=qSfQxDf<+1iC%_g_j8i^br(QV%jim5{B;~2K z5N(U;nTshQa|(@P$3;yZOeej@pbV^>A7o_Ik~#SETNgnndpWqB)agX+ z0%W+kgvOwKEr18|%U#3v&FwQpGw2u>B4EI+k(6g94&P_^pWL5a*>7*9?Z4g2B#^Xh z7Wywkc}^{#uHgwepsFFMIim77%e-H0i^2U?3+a>uZVh3L^=|H;Qm3bhe+Ap(gWc}) zhkzTHZu`3KK_E5Ai5Cq#DEIX)g8gJ!`v^G|4TO*b($K5xv~YVz(7(f}RDA*)#0ayV zDyA^VYq0Wt%_@lRpff_b7LJ$YnS^Q{13oW=HCT4!V5=X3+qVGGf2qruF5d?Y}k{p4W4mey2)dpSpp2K8H#sH{Rt}Q+W?a`CuORrk32!m#w zXNow(Fpi;{+{{CjNjf@CaKMMvHBLz&5-7s)KISdOjk7NS6 zz7HlPT#^)B|LL6ZlW+|%++xag@&{5$H5v}ALTVi993=>JrNCCiHPC*3&Omj)U^!Ba z3hd?}!wC>Glrzc^pkdn(a&AeBicRkDmnc?Z$i75a`q@i;hGS86du?CNoFbJCrG2mS z41({w`=E=9uai&Cnt8#5u`A3tBpVZ1QQ3gtr_RU|e5E8!w&{PNiWIGd_kbEx&w%4R zSb$)2&>eIx!C1W{6iC0c&C+*L7wg_ZXa7OK!cz zw#!ME6>2=r97GQt+mGv-Z{2%>2$x84mQYz);o5xNh}P{ z>U;f_4?tu*5BVf{n<1N<3>hfHR+8rQEOhNCTC+c*;J=<5*ToEeKHm@`v?Gr zWUTeMvzRT5z{lSYOh@|$my-hn2?7jeo;kj$I^Zq>mH@`zR8<95_5x4Jpt0uOQS;N_ z10d#(2Snbc1029zev>K3QwHul|0D9Y9rm_Jt#U~l! z%kknYmB?jCutu28uaaTQOblrB9c>qbc`p#qwUd6J*jVu=_|3%wBdWRg3GzjltdWhi|B(ur!Fi2GGlyxTG zK=mXi&G(suDj<<1yfNbbw=l6`9QY0}=?tt#-i1v68dBsu>r{jklac^SF~G1A9D%0M z4D@8@{k#Bzb_jzP#xx_REaxCN^+dD+aggsXLl>z*py8!2Z0ZDo_FfaKfp>$GKw`Y_ zXQ?Ou8l4pTUjCkizJG#oJQHJwXX>jytiUa(s1%K2PnvXqQ0uHoGLBvKx>YsuyFwExJD2onzKqn}-iucq! z0JDa#z*_?WDB%;ad4I@(lh;r4L(czo!ojFtj-p<&fb~mtR3-dBKn4Us0p*NvvGlZu zJP>6`96ZF3hPf)hC`m2mfrrb9f8=d_2NGa7g;7qntNKctHGkvh*(jGzFRqou@ILpw z@wXDC764PnxXL6{dpK#Fin^N{mKd*JLFpvV>R0?hy6<5Wa2*N`}Em6&4DM0Ej?uG*>@8@EF$XME&5q0Wn*mP z-)o1}2x^YadH}FQJ^s#s;>MF>GDF{213cf3@^;uEWx6(_4)SYPN_l?GAg{I+%I3?7 zBk-%i8A_WfeM0)d;+}KH@P}C`%EK+k>ZpF7ub%Fm6b$s$?}UO&9Ms7M|D6?oYSVe&qujM~L9Kadc`QH&@F()3@0Yp-J8d zP8(n*C|m77iAuU;$P6Qjn4TY3yPO_aI{zx7?)H92GI6&SYt}SPy#|464NN#r+z9nY zMnYpYT@Sy#Xd4!hYD<)&tZi^Ma&ta61VXg`24$u%xHE%` z0FWS89+Y=ZtLz{eQluzeaonG}Z0}S0sJB9^sU6P)8{0m~llZ))@@+N<8LzteBaUoH zYL`(T9bHd}nF+kLDiDZRmZ*FUjK=Cj;K>=DyJL0M{6(49=8k4JWyy}re;$F!-?0-- zBJKA^ypoI^mkLr{yv{6Nc(d>|UFnBf@_0Z2B8VwS(sOPce;=&~I_x`z99M77%zHecG2JeqbQn#tLP7X$6$}R z@lsNd7Lqe_1Bv4N&$hAxU;GeKUI{R3sA=A_C1Ueh{4xu4*`rQsdqWf4yASSV1xwz& zYDsV>U)7bruY`go6)9CCU8b0C`W#V*zmd7bF1_g@z{1l#+~P0ZXZoocZ3|bvhOC^BkG}Hl)mJd#9K--j}O+Ro2(5mjGd%gm9h9C z9@I)#?P{JjSQ03E-_YYf&8#-&nX04Ia{VF#2j09>=JfYG6W?mcn)>-Bd2ZHe?-))M zC#Ttb#XkJN;1w3=jdgUA@VO^(Ux~J>18@nwVRCoxS-5+lih#gL^D9pqV0?qGbOyZpAa}&?Bnq1G;dh+W&pjuvz^B8-xAmGoXVQ16 z+;??n&4hKox^3og|8IMIo0_9yIM#OMuWiUv=I^D{#ZoOm@V+#PyoiKSP})XK@Qo9c0_Evn3wo?+`mRLn3Bd; z_RE;q_E*!3wpng{&);qDEzD<4vnk3Vh80ypZsO^^jr{q#mD_=0F@tY(9VB1@{Q)#_ z$uUAF!g1|X-`@4)j$L`tnK@!t{uZ2{yRNq!q2{W`38pCK*n7Lo$+gI3iQ#b7yC3}< z!5HghXFzb3@MwE$q9AJ33A5$amvZb(Js_)fL6S9fhB8`K$r687t6qd?&D{M5Pm}Mk zmv;N`6_q3~rI9>Me9}XLsO#OswyA6wS3N@6W@}E_`ikYeiEZl6!lx&ON@u>@@ktkN zm^S0>X)(Ych23A-)@XIooPsq_(2|k>1TH%5x|ux*^-1d{6&ff0d zL+0pmEG`hwM${8>>x~NsJ#(;LI%koP%iZn^gt4cxfXBqU6P`Z#rl5V zv)i`tg4+0x7X+QuTj}KP1$-r{K&%6Gp@f*(t5V!ol}`GaI9!b`9=(p8rk6({4P3gr z*U~|c*12nBSrOwZ>@spKK&f#wKPx<`^EYswA9V%4k{#o{#%*sA4KRLc&CA;jTb0$<&no=>GPH1}s+O$D$srS;J9yyL?R!rTGO45?@Lx_p z8g0W?_AHqA*n#TljYYtV3?feoa>~N$Q7=NzFy%<>%eu_)PDuiy>ZZ+~8K9`@7G9N` z?Rx3sAMjSARG$13-Yfl@er>gs;t;q+jggQsyjy6t%neN$+(|EBzic$m^x#Ex5wps# z;4r82V!oBgDZ~D~QlW>BMnC5)ZRv7t%2^qF8Nm9J!xy6@OL#wWYO+z~tzyrE>hv@* zPYOmDNNL~o*Bt$tAe|lC^6==U%ykV{*MLHP1S@I<#EDXTt50VDq>4x2z4`9nSu(cE zojz~3&s|n{Y3$5xjAi;ZhfmtoA8wGor^C1U2FuA4^r;1+-0?`pkY_N~nWFj4^5bNh zl8yPsbn3m1DT8Vi?%$oxn7_DzMdqF1Rira*YJU9VD#(m_E^3u|k<~(8bDrxF_xEs5E+#zJ#r8^}W!8xo+)<$h&X@ z@fArw_A1%qgGdq-Q)%f3>DYiyDx8ya3-7N4Rq@%S-^@xlyS}|*$=l~m8(DbvE#2rv z23Z2@8Y)IRSzSKx{n~v_Xt1cW>Q_n#^(Hx(Fht*D)%Qf#2mw`_`7!OX(HyIh7hOI5 znHIC>UAv{e?}7TJrL*fW9;B#Ue9xzT45!=DH`1;o_(!aU`XyT>{D##ifWN34Bk0H5 z1)StFhscnDZucKm#wFe@nIvNCp^;l2Yth;-BmN|w_;8o95Xni4B(FJey9e7SV}(V1 zJ#T#+76cvI1vY96=-TDBtfHp2H_leBe%DA2f6*(J4b|+G<#_!J*Syh`!iuEd6y$&9 zTYN$S1%;m~-B(7luX9m{VbcL7IP2Q#wEWN$Wr^RTku-I(J>@jMvtaR&Gu|z2zIy0P zA{q{t;ZS$R$MEq&VF>qq(=S+X8zUCnobanY;Qm`{gtIZbdWFQyoC1n_c=WM62CDjL zy4F8~-ym8nb#-sVnU57;7uUKmcugY{8x@PtuGdv+r z%?GZj71fq(tnK0N!j~3@3E#ihwHdF_mo`EOs*MVoPCNTrCk9k4)UqsUYqYpFAGLW- zF6vigI-jp&5;WC%)t6SdsG|Qg{`VxT`;b&jchlAdLeAr|^BX4DYTVKSdRKLO%t20| zNL3}rByv#V614f9qv;|deNsOj*P{K`gWlyh9$|qpr*4y15G&%xo8FAGk?97#MSBON zIkA_>Ti?=OcHd$Z$r>I9qTkOI&VG2-0(!Mq!oA+q*Jqji(;Vs~F1=Vmdp;+yU8N=H zJUYvJ&YtL5yYjqNRmcN!_kHkSu8{IUV#TL({;5E)F_Qj}39)~VR2^JiYqazeqRE8J zJ~76c5^}IP6GaoN>Irk_$_5g-(;m>1My~z7GMf-9WL&MAiAXTR_>#@=M#?XCL_Q!j z1vbCVA|qF)b#JA07twPTeUVV&lUPe2{k4J7KT~Cp=T3neXys)aJ zRyKH`r0?vzQgL~^3a3%f-J-H>OTyjl0>p~gPTP=foYkMRA=FmZ3MRXcqmOwTxHmQ1 zYciTO!OYY|4BAv4iKfhRQIEFONGFvJ?;{d{REgz5$E3#{ghKk?Qi4)$Kp z%X>0+b`%-PLH>M7rnwRopX(uT*JdhI*XrGJ0{WYUFzai7FU7S(G@%gd=kYF}kvr%U z#ocfp3k9@4{@MFr(ZNKBbRjVTT!`pd)}e}|a~}R$%JEJr?3ygA>sk4UgJ39h(5C<6 z&g}MP0`ZADXjEA{>#*$pN=hCDcM#?WrH1IO+`Ii5bWKO2y0iNi2nf-ocW=8seDhJ} z6y|q#hPWOdcXb?%Q^#=$+dmu4fXFI1CR!^YGP^#X8yO9^lCz8BP zzPFAe7)|D9z2{XOQUl_e9%A)f?SexGk-AnVK1U{cDjleD7t~Q`6343(ekTF^)3F?dW@qgX!BVAk{{{aPCsGq@5ghiT08Fxn3EmrMTDZF>qjwTMW@nZ4ZM%Ssg!Kv5OBS9OmKVkjO{vh7#6SX5D_Mo~g5^ ztBv}Ff;qFVzB06KQTxOf-qX(&l4&;$y%h8S>SLX{kiz~`cl+`I*N~p5NCg-hsrGS|ym_oU)2pOim;Ote6SZ^c-aE7(IGE z#ihZA7Cyi(0blqnkNSg;hBBiU^q5kglC`hZ+Hv?FbPS7AVvGb|W+L#-11Jloia-8* z#@qRUyh8k4)4%E!_5-UhQ_#sGqzT~eI(m;DK}~5ZP*pi|XTLestUu5H>TiLNe8<`M zG4IsprZgX#y-NYTJpunt0#ZxC-9fhM?aJg4Xp}wncxfA$WPD5_*I!#put}A@44lI; zGA88Ri04PH)f;J+V^f;9kE%JkMH;GnluSHMhzlFCX^{6pJ}Bf#ICn=8NZ3YL zw3DuD8>*f21+r@VGsOKEB?VXUTSrx*?7i0q8d+nld)>0b%EfPDCYtnKH||bc{ceUleo(l# z{R>4ZV85HRfTnsm;Ll$8inF@-H3Pj@TxxofEX?-H?vYKSayc-(>1ZZxx`nT`mLj%< z+j(B}BIA?prAnRV_I;7Zav5)I)|(srPFER`XL={M?D=cC`OJ`{xWkhvC0U*iuao=m z{`3BK(8Y$!EG?EPg*SsN_wN$HkT*;5GGA!QZykeD!((jZ?_n`!I>9ZPUn^M`QG%#N zw@~LnxdBppBLjpQ4RO0Jqb%1HC?KaHf=O|pL@`}miSu0KrPNP?pWUhVXGcB{Wvux3 zJ&-kCa*9q#LG>1mXTSKt82vjvw`8}-J16NjhZHR84H6jP)G-vB7-w}GSKDl|`OVED zKM4+zGf%H^vnPI5aLLb;PY$VZVY_^`WSY)4nZKF&v%LT@jSfGuI1H{nf3hcSCk!w2 zK8OkCPi?wA_y*jrSt1qrQhhOM)4$^`x5UwE%Q-u#GJGkNEM>v0)bY%@RY4)U%AAPv~7}?2PfAzeM zFKt9|bDO;%QM(si;;h;g#(BGL8j5ApZeDA*_pnEvi}5-=xU%u2 zs!Zvi4-_owc}4CX8|Jmb%&L#c2Z|fAp1^ZT7~Ijo$QPW9>%w5BT#YTTBL#wQqr~{L zL{v7jZSPqcxXW$rafya&n&M0w-68k=XC@F;Bbi988#^C#MIGTLzVm#AMfhYQzKHKU zZ)DcI8mvVeBF&7lYW%{407L(gM1Dvd~GW_10887?i+lMcj*VnXoCx5DSonpI!&!MF}l&PL6+A z$L!7AP{*rpF1y-ZX0F>AM-FM+U_#1^&8!YBj�MEsfSEJc?6qjCTdMZ`}9o2s7sE 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 \ No newline at end of file diff --git a/packages/vscode-ide-companion/resources/faq1.gif b/packages/vscode-ide-companion/resources/faq1.gif new file mode 100644 index 0000000000000000000000000000000000000000..93d694497af26e6122e1e3ff50940990bbf84953 GIT binary patch literal 186246 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⪙ouSZ)M+%iGF!$ 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 { + const reader = new FileReader(); + reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : ""); + reader.onerror = () => reject(reader.error || new Error("Failed to read file.")); + reader.readAsDataURL(file); + }); + } + + function pickImageFileFromClipboard(event) { + const items = Array.from(event.clipboardData?.items || []); + for (const item of items) { + if (item.kind !== "file") { + continue; + } + const file = item.getAsFile(); + if (isImageFile(file)) { + return file; + } + } + return null; + } + + function createPromptAttachmentManager(options) { + const promptInput = options?.promptInput; + const inputWrap = options?.inputWrap; + const toolsLine = options?.toolsLine; + const onAttachmentChange = + typeof options?.onAttachmentChange === "function" ? options.onAttachmentChange : function () {}; + + if (!promptInput || !inputWrap || !toolsLine) { + throw new Error("Prompt attachment manager requires promptInput, inputWrap, and toolsLine."); + } + + let attachment = null; + let previewPopup = null; + let previewImage = null; + + function ensurePreviewPopup() { + if (previewPopup) { + return; + } + + previewPopup = createElement("div", "chat-attached-context-preview"); + previewImage = createElement("img", "chat-attached-context-preview-image"); + previewImage.alt = ATTACHMENT_LABEL; + previewPopup.appendChild(previewImage); + document.body.appendChild(previewPopup); + } + + function hidePreview() { + if (!previewPopup) { + return; + } + previewPopup.classList.remove("show"); + } + + function updatePreviewPosition(anchor) { + if (!previewPopup || !anchor) { + return; + } + + const rect = anchor.getBoundingClientRect(); + const popupRect = previewPopup.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + let left = rect.left; + let top = rect.top - popupRect.height - PREVIEW_OFFSET; + + if (left + popupRect.width > viewportWidth - 12) { + left = viewportWidth - popupRect.width - 12; + } + if (left < 12) { + left = 12; + } + if (top < 12) { + top = rect.bottom + PREVIEW_OFFSET; + } + if (top + popupRect.height > viewportHeight - 12) { + top = Math.max(12, viewportHeight - popupRect.height - 12); + } + + previewPopup.style.left = left + "px"; + previewPopup.style.top = top + "px"; + } + + function showPreview(anchor) { + if (!attachment) { + return; + } + + ensurePreviewPopup(); + previewImage.src = attachment.dataUrl; + previewPopup.classList.add("show"); + updatePreviewPosition(anchor); + } + + function emitChange() { + onAttachmentChange({ + hasAttachments: Boolean(attachment), + attachments: attachment ? [attachment] : [], + }); + } + + function clear() { + attachment = null; + toolsLine.innerHTML = ""; + toolsLine.classList.remove("has-attachment"); + hidePreview(); + emitChange(); + } + + function createAttachmentNode() { + const wrapper = createElement("div", "chat-attached-context-attachment show-file-icons"); + wrapper.tabIndex = 0; + wrapper.setAttribute("role", "button"); + wrapper.setAttribute("aria-label", ATTACHMENT_LABEL + " (删除)"); + wrapper.draggable = true; + + const removeButton = createElement("a", "monaco-button codicon codicon-close"); + removeButton.tabIndex = -1; + removeButton.setAttribute("role", "button"); + removeButton.setAttribute("aria-label", "从上下文中移除"); + removeButton.href = "#"; + removeButton.textContent = "×"; + removeButton.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + clear(); + }); + + const iconLabel = createElement("div", "monaco-icon-label"); + const iconLabelContainer = createElement("div", "monaco-icon-label-container"); + const iconNameContainer = createElement("span", "monaco-icon-name-container"); + iconLabelContainer.appendChild(iconNameContainer); + iconLabel.appendChild(iconLabelContainer); + + const pill = createElement("div", "chat-attached-context-pill"); + const image = createElement("img", "chat-attached-context-pill-image"); + image.src = attachment.dataUrl; + image.alt = ATTACHMENT_LABEL; + pill.appendChild(image); + + const text = createElement("span", "chat-attached-context-custom-text"); + text.textContent = ATTACHMENT_LABEL; + + wrapper.appendChild(removeButton); + wrapper.appendChild(iconLabel); + wrapper.appendChild(pill); + wrapper.appendChild(text); + + const show = () => showPreview(wrapper); + wrapper.addEventListener("mouseenter", show); + wrapper.addEventListener("focus", show); + wrapper.addEventListener("mouseleave", hidePreview); + wrapper.addEventListener("blur", hidePreview); + wrapper.addEventListener("dragstart", (event) => { + event.preventDefault(); + }); + wrapper.addEventListener("keydown", (event) => { + if (event.key === "Delete" || event.key === "Backspace") { + event.preventDefault(); + clear(); + } + }); + + return wrapper; + } + + function render() { + toolsLine.innerHTML = ""; + toolsLine.classList.toggle("has-attachment", Boolean(attachment)); + if (!attachment) { + hidePreview(); + return; + } + toolsLine.appendChild(createAttachmentNode()); + } + + function setAttachmentData(data) { + if (!data?.dataUrl) { + return false; + } + + attachment = { + name: data.name || ATTACHMENT_LABEL, + mimeType: data.mimeType || "image/png", + dataUrl: data.dataUrl, + label: ATTACHMENT_LABEL, + }; + render(); + emitChange(); + return true; + } + + async function setAttachmentFromFile(file) { + if (!isImageFile(file)) { + return false; + } + + const dataUrl = await readFileAsDataUrl(file); + return setAttachmentData({ + name: file.name || ATTACHMENT_LABEL, + mimeType: file.type || "image/png", + dataUrl, + label: ATTACHMENT_LABEL, + }); + } + + async function handlePaste(event) { + const file = pickImageFileFromClipboard(event); + if (!file) { + return; + } + + event.preventDefault(); + try { + await setAttachmentFromFile(file); + } catch (error) { + console.error("Failed to attach pasted image.", error); + } + } + + promptInput.addEventListener("paste", handlePaste); + + window.addEventListener("resize", () => { + const attachmentNode = toolsLine.querySelector(".chat-attached-context-attachment"); + if (previewPopup?.classList.contains("show") && attachmentNode) { + updatePreviewPosition(attachmentNode); + } + }); + + window.addEventListener( + "scroll", + () => { + const attachmentNode = toolsLine.querySelector(".chat-attached-context-attachment"); + if (previewPopup?.classList.contains("show") && attachmentNode) { + updatePreviewPosition(attachmentNode); + } + }, + true + ); + + return { + clear, + hasAttachments() { + return Boolean(attachment); + }, + getImageUrls() { + return attachment ? [attachment.dataUrl] : []; + }, + }; + } + + window.createPromptAttachmentManager = createPromptAttachmentManager; +})(); diff --git a/packages/vscode-ide-companion/resources/webview.css b/packages/vscode-ide-companion/resources/webview.css new file mode 100644 index 00000000..ea1d71a2 --- /dev/null +++ b/packages/vscode-ide-companion/resources/webview.css @@ -0,0 +1,1604 @@ +/* CSS Variables */ +:root { + --bg: var(--vscode-editor-background); + --panel: var(--vscode-sideBar-background); + --panel-2: var(--vscode-editor-inactiveSelectionBackground); + --muted: var(--vscode-descriptionForeground); + --accent: var(--vscode-focusBorder); + --accent-2: var(--vscode-button-background); + --danger: var(--vscode-errorForeground); + --shadow: rgba(0, 0, 0, 0.15); + --border-color: var(--vscode-panel-border); + --prompt-line-height: 18px; + --prompt-min-height: calc(var(--prompt-line-height) * 3 + 20px); + --prompt-max-height: calc(var(--prompt-line-height) * 10 + 20px); +} + +/* Global Styles */ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + height: 100vh; + overflow-x: hidden; + background: var(--bg); + color: var(--vscode-foreground); + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); +} + +/* App Container */ +.app { + height: 100vh; + display: flex; + flex-direction: column; + position: relative; + overflow-x: hidden; +} + +/* Header Container */ +.header-container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 4px 16px; + background: var(--vscode-sideBar-background); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; + position: relative; +} + +.header-left { + position: relative; + min-width: 0; +} + +/* Session Selector */ +.session-selector { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s; + width: 100%; +} + +.session-selector:hover { + background: var(--vscode-toolbar-hoverBackground); +} + +.session-selector.open .session-selector-icon { + transform: rotate(180deg); +} + +.session-selector-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--vscode-foreground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; +} + +.session-logo { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.session-title-text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.session-selector-icon { + width: 16px; + height: 16px; + fill: var(--vscode-foreground); + transition: transform 0.2s; + flex-shrink: 0; +} + +/* Header New Button */ +.header-new-btn { + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background 0.2s; + background: none; + display: flex; + align-items: center; + justify-content: center; + padding: 6px; + flex-shrink: 0; +} + +.header-new-btn:hover { + background: var(--vscode-toolbar-hoverBackground); +} + +.header-new-icon { + width: 16px; + height: 16px; + fill: var(--vscode-foreground); +} + +/* Session Dropdown */ +.session-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 16px; + right: 16px; + background: var(--vscode-dropdown-background); + border: 1px solid var(--border-color); + border-radius: 4px; + max-height: 400px; + z-index: 10000; + display: none; + box-shadow: 0 4px 12px var(--shadow); + flex-direction: column; +} + +.session-dropdown.show { + display: flex; +} + +.session-search-box { + padding: 8px; + flex-shrink: 0; +} + +.session-search-input { + width: 100%; + padding: 6px 10px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + font-size: 13px; + outline: none; + font-family: var(--vscode-font-family); + outline: none; +} + +.session-search-input:focus { + border-color: var(--vscode-focusBorder); + outline: none; +} + +.session-search-input::placeholder { + color: var(--vscode-input-placeholderForeground); +} + +.session-dropdown-list { + flex: 1; + overflow-y: auto; + min-height: 0; + padding: 8px 12px 12px; +} + +.session-dropdown-group { + margin-bottom: 12px; +} + +.session-dropdown-group:last-child { + margin-bottom: 0; +} + +.session-dropdown-group-title { + font-size: 11px; + font-weight: 500; + color: var(--vscode-descriptionForeground); + letter-spacing: 0.5px; + padding: 6px 6px 8px; + margin-bottom: 2px; +} + +.session-dropdown-empty { + padding: 20px; + text-align: center; + color: var(--vscode-descriptionForeground); + font-size: 13px; +} + +.session-dropdown-item { + padding: 6px; + cursor: pointer; + transition: background 0.2s; + display: flex; + gap: 12px; + margin-bottom: 6px; + border-radius: 4px; + align-items: center; +} + +.session-dropdown-item:last-child { + margin-bottom: 0; +} + +.session-dropdown-item:hover { + background: var(--vscode-list-hoverBackground); +} + +.session-dropdown-item.active { + background: var(--vscode-list-activeSelectionBackground); +} + +.session-dropdown-item.active .session-dropdown-summary { + color: var(--vscode-list-activeSelectionForeground); +} + +.session-dropdown-item.active .session-dropdown-time { + color: var(--vscode-list-activeSelectionForeground); + opacity: 0.8; +} + +.session-dropdown-summary { + font-size: 13px; + color: var(--vscode-foreground); + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + font-weight: 500; +} + +.session-dropdown-time { + font-size: 11px; + color: var(--vscode-descriptionForeground); +} + +.session-dropdown-summary mark { + background-color: var(--vscode-editor-findMatchHighlightBackground); + color: var(--vscode-editor-foreground); + padding: 0 2px; + border-radius: 2px; +} + +/* Chat Container */ +.chat-container { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + overflow-x: hidden; +} + +.chat-container.hidden { + display: none; +} + +/* Messages */ +.messages { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 16px; + display: flex; + flex-direction: column; + gap: 1px; + background: var(--vscode-sideBar-background); +} + +/* Message Bubbles */ +.bubble { + padding: 16px; + font-size: 13px; + width: 100%; + border: 1px solid transparent; + color: var(--vscode-foreground); + border-radius: 6px; + position: relative; + overflow-wrap: break-word; + word-break: break-word; +} + +/* Collapsible Bubble */ +.bubble-collapsible-header { + display: flex; + align-items: flex-start; + gap: 8px; + cursor: pointer; + user-select: none; + padding: 4px 0; + position: relative; +} + +.bubble-collapsible-header:hover .bubble-title-text { + opacity: 0.8; +} + +.bubble-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--vscode-descriptionForeground); + flex-shrink: 0; + margin-top: 6px; + position: relative; + z-index: 3; +} + +.bubble-dot.success { + background-color: var(--vscode-terminal-ansiGreen); +} + +.bubble-dot.error { + background-color: var(--vscode-terminal-ansiRed); +} + +/* 连接线:只在有 connect-to-prev class 时显示向上的连接线 */ +.bubble-dot.connect-to-prev::before { + content: ""; + position: absolute; + left: 3.5px; + bottom: 8px; + width: 1px; + height: var(--line-height, 0px); + background-color: var(--vscode-panel-border); + z-index: 0; +} + +.bubble-title { + flex: 1; + font-size: 13px; + color: var(--vscode-foreground); + display: flex; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + min-width: 0; + overflow: visible; +} + +.bubble-title-text { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; + overflow: visible; +} + +.bubble-title-text b { + text-transform: capitalize; + flex-shrink: 0; +} + +.bubble-title .tool-params { + color: var(--vscode-descriptionForeground); + font-weight: normal; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; + position: relative; + display: inline-block; +} + +/* Tooltip styles */ +.tooltip { + position: fixed; + padding: 4px 8px; + background: var(--vscode-editorHoverWidget-background); + color: var(--vscode-editorHoverWidget-foreground); + border: 1px solid var(--vscode-editorHoverWidget-border); + border-radius: 4px; + font-size: 12px; + white-space: normal; + word-break: break-word; + max-width: 400px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 10000; + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: + opacity 0.5s, + visibility 0.5s; +} + +.tooltip.show { + opacity: 1; + visibility: visible; +} + +.bubble-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--vscode-descriptionForeground); + font-size: 12px; +} + +.bubble-toggle svg { + width: 12px; + height: 12px; + fill: var(--vscode-descriptionForeground); + transition: transform 0.2s; +} + +.bubble-toggle.expanded svg { + transform: rotate(180deg); +} + +.bubble-collapsible-content { + margin-top: 8px; + margin-left: 24px; + padding: 8px 12px; + background: var(--vscode-textCodeBlock-background); + border: 1px solid var(--border-color); + border-radius: 4px; + overflow: auto; + max-width: calc(100% - 24px); + font-family: var(--vscode-editor-font-family); + font-size: 12px; + white-space: pre-wrap; + word-break: break-word; + max-height: 300px; +} + +.tool-result-details { + display: flex; + flex-direction: column; + gap: 10px; + font-family: var(--vscode-font-family); + white-space: normal; +} + +.tool-result-summary { + color: var(--vscode-foreground); + line-height: 1.5; +} + +.update-plan-result { + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + font-family: var(--vscode-font-family); + font-size: 13px; + line-height: 1.45; + padding: 10px; + white-space: normal; +} + +.update-plan-heading { + color: var(--vscode-foreground); + font-size: 13px; + font-weight: 650; + line-height: 1.35; + margin: 0 0 8px; +} + +.update-plan-heading:not(:first-child) { + margin-top: 12px; +} + +.update-plan-spacer { + height: 6px; +} + +.update-plan-paragraph { + margin: 4px 0; +} + +.update-plan-task, +.update-plan-bullet { + display: grid; + grid-template-columns: 18px 1fr; + gap: 8px; + align-items: flex-start; + margin: 5px 0; + padding-left: calc(var(--plan-indent, 0) * 18px); +} + +.update-plan-status { + width: 14px; + height: 14px; + margin-top: 2px; + border: 1px solid var(--vscode-descriptionForeground); + border-radius: 50%; + color: var(--vscode-editor-background); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 700; + line-height: 1; +} + +.update-plan-task.status-done .update-plan-status { + background: var(--vscode-testing-iconPassed, #2ea043); + border-color: var(--vscode-testing-iconPassed, #2ea043); +} + +.update-plan-task.status-active .update-plan-status { + background: var(--vscode-charts-blue, #3794ff); + border-color: var(--vscode-charts-blue, #3794ff); +} + +.update-plan-task.status-attention .update-plan-status { + background: var(--vscode-testing-iconFailed, #f85149); + border-color: var(--vscode-testing-iconFailed, #f85149); +} + +.update-plan-task.status-todo .update-plan-status { + background: transparent; +} + +.update-plan-task.status-done .update-plan-task-text { + color: var(--vscode-descriptionForeground); + text-decoration: line-through; +} + +.update-plan-bullet-marker { + color: var(--vscode-descriptionForeground); + line-height: 1.45; + text-align: center; +} + +.update-plan-markdown code { + border-radius: 3px; + background: var(--vscode-textCodeBlock-background); + font-family: var(--vscode-editor-font-family); + font-size: 0.92em; + padding: 1px 4px; +} + +.tool-result-file-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.tool-result-label { + color: var(--vscode-descriptionForeground); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.tool-result-filepath { + border: none; + background: none; + color: var(--vscode-textLink-foreground, var(--vscode-button-background)); + cursor: pointer; + font: inherit; + padding: 0; + text-decoration: underline; + text-underline-offset: 2px; + word-break: break-all; +} + +.tool-result-filepath:hover { + color: var(--vscode-textLink-activeForeground, var(--vscode-button-hoverBackground)); +} + +.tool-result-diff { + display: flex; + flex-direction: column; + gap: 6px; +} + +.diff-preview { + border: 1px solid var(--border-color); + border-radius: 6px; + overflow: hidden; + background: var(--vscode-editor-background); +} + +.diff-line { + display: grid; + grid-template-columns: 20px 1fr; + gap: 0; + font-family: var(--vscode-editor-font-family); + font-size: 12px; + line-height: 1.2; +} + +.diff-line-marker { + display: flex; + align-items: flex-start; + justify-content: center; + padding: 1px 0; + color: var(--vscode-descriptionForeground); + user-select: none; +} + +.diff-line-content { + display: block; + padding: 1px 10px 1px 0; + white-space: pre-wrap; + word-break: break-word; +} + +.diff-line.added { + background: color-mix( + in srgb, + var(--vscode-diffEditor-insertedLineBackground, rgba(46, 160, 67, 0.18)) 100%, + transparent + ); +} + +.diff-line.removed { + background: color-mix( + in srgb, + var(--vscode-diffEditor-removedLineBackground, rgba(248, 81, 73, 0.16)) 100%, + transparent + ); +} + +.diff-line.context { + background: var(--vscode-editor-background); +} + +/* assistant 角色的展开内容无边框 */ +.bubble.assistant .bubble-collapsible-content { + border: none; + background: transparent; + padding: 8px 0; +} + +.bubble-collapsible-content.collapsed { + display: none; +} + +/* 普通assistant气泡布局 */ +.bubble.assistant:has(.bubble-normal-content) { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.bubble.assistant:has(.bubble-normal-content) > .bubble-dot { + margin-top: 6px; +} + +.bubble-normal-content { + flex: 1; + min-width: 0; +} + +/* 复制按钮 */ +.bubble-copy-btn { + display: none; + position: absolute; + top: 8px; + right: 8px; + width: 26px; + height: 26px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--vscode-editor-background); + color: var(--vscode-descriptionForeground); + cursor: pointer; + align-items: center; + justify-content: center; + transition: + opacity 0.15s, + background 0.15s; + z-index: 5; +} + +.bubble-copy-btn:hover { + background: var(--vscode-toolbar-hoverBackground); + color: var(--vscode-foreground); +} + +.bubble-copy-btn.copied { + color: var(--vscode-testing-iconPassed, #2ea043); + border-color: var(--vscode-testing-iconPassed, #2ea043); +} + +.bubble-copy-btn svg { + width: 14px; + height: 14px; + fill: currentColor; + pointer-events: none; +} + +.bubble.assistant:hover .bubble-copy-btn { + display: flex; +} + +.bubble.user { + background: var(--vscode-editor-background); + border-color: var(--vscode-panel-border); + padding: 4px 6px; + margin: 12px 0 12px auto; + white-space: pre-wrap; + word-break: break-word; + width: fit-content; + max-width: 80%; +} + +.bubble.system { + background: transparent; + border-color: transparent; + padding: 4px 16px; +} + +.bubble.assistant { + background: transparent; + border-color: transparent; + padding: 4px 16px; +} + +.bubble.tool { + background: transparent; + border-color: transparent; + padding: 4px 16px; +} + +.bubble p { + margin: 8px 0; + overflow-wrap: break-word; + word-break: break-word; +} + +.bubble p:first-of-type { + margin-top: 0; +} + +.bubble p:last-of-type { + margin-bottom: 0; +} + +.bubble pre { + padding: 12px; + border-radius: 4px; + overflow-x: auto; + max-width: 100%; + color: var(--vscode-editor-foreground); + background: var(--vscode-textCodeBlock-background); + margin: 8px 0; + border: 1px solid var(--vscode-panel-border); + white-space: pre-wrap; + word-break: break-word; +} + +.bubble code { + font-family: var(--vscode-editor-font-family); + font-size: 12px; + color: var(--vscode-editor-foreground); + background-color: unset; + overflow-wrap: break-word; + word-break: break-word; +} + +.ask-user-question { + display: flex; + flex-direction: column; + gap: 12px; + font-family: var(--vscode-font-family); + font-size: 13px; + white-space: normal; +} + +.ask-user-question-intro { + color: var(--vscode-descriptionForeground); +} + +.ask-user-question-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.ask-user-question-content { + overflow: visible; + max-height: none; +} + +.ask-user-question-block { + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 12px; + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.ask-user-question-title { + padding: 0 4px; + font-weight: 600; +} + +.ask-user-question-option { + display: flex; + align-items: flex-start; + gap: 8px; + cursor: pointer; +} + +.ask-user-question-option input { + margin: 2px 0 0; +} + +.ask-user-question-option-text { + display: flex; + flex-direction: column; + gap: 2px; +} + +.ask-user-question-option-description { + color: var(--vscode-descriptionForeground); + font-size: 12px; +} + +.ask-user-question-other { + display: flex; + flex-direction: column; + gap: 6px; +} + +.ask-user-question-other-label { + font-size: 12px; + color: var(--vscode-descriptionForeground); +} + +.ask-user-question-other textarea { + min-height: 56px; + max-height: 120px; + resize: none; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); +} + +.ask-user-question-actions { + margin-top: 10px; +} + +.ask-user-question-error { + color: var(--vscode-errorForeground); + font-size: 12px; + display: none; + margin-bottom: 6px; +} + +.ask-user-question-error:not(:empty) { + display: block; +} + +.ask-user-question-submit { + align-self: flex-start; + border: 1px solid transparent; + border-radius: 4px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + padding: 6px 12px; + cursor: pointer; + font-size: 12px; +} + +.ask-user-question-submit:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.ask-user-question-empty { + color: var(--vscode-descriptionForeground); +} + +.permission-prompt-host { + margin-bottom: 10px; +} + +.permission-card { + border: 1px solid var(--vscode-panel-border); + border-left: 3px solid var(--vscode-notificationsWarningIcon-foreground, #f59e0b); + border-radius: 6px; + background: var(--vscode-input-background); + box-shadow: 0 4px 14px var(--shadow); + padding: 12px; +} + +.permission-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.permission-title { + color: var(--vscode-notificationsWarningIcon-foreground, #f59e0b); + font-weight: 700; +} + +.permission-progress { + margin-top: 2px; + color: var(--vscode-descriptionForeground); + font-size: 11px; +} + +.permission-close { + border: none; + border-radius: 4px; + background: transparent; + color: var(--vscode-descriptionForeground); + cursor: pointer; + font-size: 18px; + line-height: 1; + padding: 0 4px; +} + +.permission-close:hover { + background: var(--vscode-toolbar-hoverBackground); + color: var(--vscode-foreground); +} + +.permission-tool { + margin-top: 10px; + font-weight: 700; +} + +.permission-command { + margin-top: 6px; + border-radius: 4px; + background: var(--vscode-textCodeBlock-background); + color: var(--vscode-editor-foreground); + font-family: var(--vscode-editor-font-family); + font-size: 12px; + line-height: 1.45; + padding: 8px; + white-space: pre-wrap; + word-break: break-word; +} + +.permission-description { + margin-top: 8px; + color: var(--vscode-descriptionForeground); + line-height: 1.45; +} + +.permission-scope { + display: inline-flex; + align-items: center; + margin-top: 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + padding: 3px 8px; +} + +.permission-scope.risk-low { + background: rgba(34, 197, 94, 0.14); + color: #22c55e; +} + +.permission-scope.risk-medium { + background: rgba(245, 158, 11, 0.14); + color: #f59e0b; +} + +.permission-scope.risk-high { + background: rgba(239, 68, 68, 0.14); + color: #ef4444; +} + +.permission-question { + margin-top: 10px; + font-weight: 600; +} + +.permission-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.permission-action { + border: 1px solid var(--vscode-button-border, transparent); + border-radius: 4px; + background: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + cursor: pointer; + font-size: 12px; + padding: 6px 10px; +} + +.permission-action:hover { + background: var(--vscode-button-secondaryHoverBackground); +} + +.permission-allow, +.permission-always { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.permission-allow:hover, +.permission-always:hover { + background: var(--vscode-button-hoverBackground); +} + +.permission-deny { + border-color: var(--vscode-inputValidation-errorBorder, #ef4444); + color: var(--vscode-errorForeground, #ef4444); +} + +.permission-denied-card { + border-left-color: var(--vscode-errorForeground, #ef4444); +} + +.permission-denied-card .permission-title { + color: var(--vscode-errorForeground, #ef4444); +} + +/* Spinner dot for live thinking bubble */ +.bubble-dot.spinner-dot { + background: transparent !important; +} + +/* 旋转环用 ::after 实现,不影响 ::before 连线 */ +.bubble-dot.spinner-dot::after { + content: ""; + position: absolute; + top: -2px; + left: -2px; + width: calc(100% + 4px); + height: calc(100% + 4px); + border-radius: 50%; + border: 2px solid var(--vscode-progressBar-background); + border-top-color: var(--accent); + animation: spin 0.8s linear infinite; + box-sizing: border-box; + pointer-events: none; + z-index: 1; +} + +/* Thinking bubble: 标题和内容同行显示 */ +.bubble[data-thinking-live="true"] .thinking-status { + color: var(--vscode-descriptionForeground); + font-weight: normal; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Composer */ +.composer { + position: relative; + padding: 12px 16px 16px; + background: var(--vscode-sideBar-background); + flex-shrink: 0; +} + +.input-wrap { + position: relative; + display: flex; + flex-direction: column; + min-height: calc(var(--prompt-min-height) + 42px); + border-radius: 4px; + border: 1px solid var(--border-color); + background: var(--vscode-input-background); + transition: + border-color 0.15s ease, + outline-color 0.15s ease, + background-color 0.15s ease; +} + +.input-wrap:focus-within { + border-color: var(--vscode-focusBorder); + outline: 1px solid var(--vscode-focusBorder); +} + +.tools-line { + display: none; + align-items: center; + gap: 8px; + min-height: 0; + padding: 0 12px; +} + +.tools-line.has-attachment { + display: flex; + min-height: 34px; + padding: 10px 12px 0; +} + +.composer-footer { + display: flex; + height: 42px; + width: 100%; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 6px 12px; + position: relative; +} + +/* Textarea */ +textarea { + width: 100%; + min-height: var(--prompt-min-height); + max-height: var(--prompt-max-height); + resize: none; + overflow-y: hidden; + border: none; + border-radius: 4px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + padding: 10px 12px; + font-size: 13px; + line-height: var(--prompt-line-height); + outline: none; + font-family: var(--vscode-font-family); +} + +textarea:focus { + outline: none; +} + +textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} + +.chat-attached-context-attachment { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + max-width: 100%; + padding: 4px; + border: 1px solid var(--vscode-editorWidget-background, var(--vscode-input-background)); + border-radius: 6px; + color: var(--vscode-foreground); +} + +.chat-attached-context-attachment:hover { + border-color: var(--vscode-focusBorder); +} + +.chat-attached-context-attachment .monaco-button.codicon-close { + display: inline-flex; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 50%; + color: var(--vscode-descriptionForeground); + text-decoration: none; + flex-shrink: 0; +} + +.chat-attached-context-attachment .monaco-button.codicon-close:hover { + background: var(--vscode-toolbar-hoverBackground); + color: var(--vscode-foreground); +} + +.monaco-icon-label, +.monaco-icon-label-container, +.monaco-icon-name-container { + display: none; +} + +.chat-attached-context-pill { + width: 13px; + height: 13px; + border-radius: 4px; + overflow: hidden; + flex-shrink: 0; + background: var(--vscode-editor-background); +} + +.chat-attached-context-pill-image { + display: block; + width: 13px; + height: 13px; + margin: 1.5px; + object-fit: cover; + border-radius: 2px; +} + +.chat-attached-context-custom-text { + font-size: 12px; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-attached-context-preview { + position: fixed; + display: none; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 12px; + background: var(--vscode-editorHoverWidget-background, var(--vscode-editorWidget-background)); + box-shadow: 0 8px 24px var(--shadow); + z-index: 10001; + pointer-events: none; +} + +.chat-attached-context-preview.show { + display: block; +} + +.chat-attached-context-preview-image { + display: block; + max-width: min(520px, 70vw); + max-height: min(420px, 65vh); + border-radius: 6px; + object-fit: contain; +} + +/* Send Button */ +.send-button { + width: 30px; + height: 30px; + border-radius: 50%; + border: none; + cursor: pointer; + display: grid; + place-items: center; + background: transparent; +} + +.context-meter { + position: relative; + width: 18px; + height: 18px; + display: grid; + place-items: center; + flex-shrink: 0; + z-index: 3; +} + +.context-meter-ring { + --context-percent: 0%; + width: 14px; + height: 14px; + border-radius: 50%; + background: + radial-gradient(circle at center, var(--vscode-input-background) 36%, transparent 38%), + conic-gradient( + var(--vscode-descriptionForeground) 0 var(--context-percent), + var(--vscode-editorWidget-border, var(--vscode-panel-border)) var(--context-percent) 100% + ); +} + +.context-meter:hover .context-meter-ring { + filter: brightness(1.1); +} + +.context-meter-tooltip { + position: fixed; + left: 16px; + bottom: calc(100% + 10px); + display: none; + width: min(360px, calc(100vw - 32px)); + max-height: min(420px, 70vh); + overflow: auto; + padding: 12px 14px; + border: 1px solid var(--vscode-editorHoverWidget-border, var(--border-color)); + border-radius: 12px; + background: var(--vscode-editorHoverWidget-background, var(--vscode-editorWidget-background)); + color: var(--vscode-editorHoverWidget-foreground, var(--vscode-foreground)); + box-shadow: 0 8px 24px var(--shadow); + font-size: 12px; + line-height: 1.4; + pointer-events: none; + z-index: 10002; +} + +.context-meter:hover .context-meter-tooltip { + display: block; +} + +.context-tooltip-title { + text-align: center; + color: var(--vscode-descriptionForeground); + font-size: 13px; + margin-bottom: 4px; +} + +.context-tooltip-summary { + text-align: center; + font-size: 16px; + font-weight: 600; + margin-bottom: 10px; +} +.context-tooltip-section { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid var(--border-color); + color: var(--vscode-descriptionForeground); + font-weight: 600; +} + +.context-tooltip-row { + display: flex; + justify-content: space-between; + gap: 12px; + margin-top: 4px; +} + +.context-tooltip-label { + min-width: 0; + color: var(--vscode-descriptionForeground); + overflow-wrap: anywhere; +} + +.context-tooltip-value { + max-width: 55%; + text-align: right; + font-family: var(--vscode-editor-font-family); + overflow-wrap: anywhere; +} + +.send-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.send-icon { + width: 26px; + height: 26px; + fill: currentColor; + color: var(--vscode-foreground); + transition: opacity 0.2s; +} + +#sendIcon { + color: var(--vscode-descriptionForeground); +} + +#stopIcon { + display: none; + color: var(--vscode-foreground); +} + +.send-icon.empty { + opacity: 0.6; +} + +/* Skills Button */ +.skills-button { + height: 22px; + border-radius: 4px; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + background: transparent; + padding: 0; + transition: opacity 0.2s; + flex-shrink: 0; + color: var(--vscode-foreground); +} + +.skills-button span { + margin-top: 4px; +} + +.skills-button:hover { + opacity: 0.8; +} + +.skills-button svg { + width: 16px; + height: 16px; + fill: var(--vscode-foreground); +} + +/* Skills Popup */ +.skills-popup { + position: absolute; + bottom: calc(100% + 8px); + left: 16px; + right: 16px; + background: var(--vscode-dropdown-background); + border: 1px solid var(--border-color); + border-radius: 4px; + max-height: 300px; + overflow-y: auto; + z-index: 10000; + display: none; + box-shadow: 0 4px 12px var(--shadow); +} + +.skills-popup.show { + display: block; +} + +.skills-popup-header { + padding: 8px 12px; + font-size: 12px; + font-weight: 500; + color: var(--vscode-descriptionForeground); + border-bottom: 1px solid var(--border-color); +} + +.skills-popup-list { + padding: 8px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.skills-popup-empty { + padding: 12px; + text-align: center; + color: var(--vscode-descriptionForeground); + font-size: 13px; +} + +.skills-popup-item { + display: flex; + align-items: center; + padding: 6px 10px; + cursor: pointer; + border-radius: 4px; + font-size: 13px; + transition: background 0.2s; + gap: 8px; +} + +.skills-popup-item:hover { + background: var(--vscode-list-hoverBackground); +} + +.skills-popup-item.selected { + background: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); +} + +.skills-popup-item-name { + flex: 0 0 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.skills-popup-item-path { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--vscode-descriptionForeground); + font-size: 12px; +} + +.skills-popup-item:hover .skills-popup-item-path, +.skills-popup-item.selected .skills-popup-item-path { + color: inherit; +} + +.skills-popup-item-loaded { + color: var(--vscode-testing-icon-success); + font-weight: bold; + font-size: 14px; + margin-left: 4px; +} + +/* Skills Bar (contains button and tags) */ +.skills-bar { + display: flex; + align-items: center; + gap: 8px; + width: calc(100% - 60px); +} + +/* Skills Tags */ +.skills-tags { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; +} + +.skills-tags-inner { + width: 100%; + overflow: auto; + white-space: nowrap; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.skills-tags-inner::-webkit-scrollbar { + width: 0; + height: 0; +} + +.skill-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 6px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border-radius: 4px; + font-size: 12px; + margin-right: 4px; +} + +.skill-tag-name { + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.skill-tag-remove { + cursor: pointer; + font-size: 14px; + line-height: 1; + opacity: 0.7; + transition: opacity 0.2s; +} + +.skill-tag-remove:hover { + opacity: 1; +} + +/* Prevent horizontal overflow for all markdown content */ +.bubble table { + display: block; + overflow-x: auto; + max-width: 100%; +} + +.bubble img { + max-width: 100%; + height: auto; +} + +.bubble ul, +.bubble ol { + padding-left: 1.5em; + overflow-wrap: break-word; +} + +.bubble li { + overflow-wrap: break-word; + word-break: break-word; +} + +.bubble blockquote { + overflow-wrap: break-word; + word-break: break-word; +} + +.bubble a { + overflow-wrap: break-word; + word-break: break-all; +} diff --git a/packages/vscode-ide-companion/resources/webview.html b/packages/vscode-ide-companion/resources/webview.html new file mode 100644 index 00000000..1290f8d6 --- /dev/null +++ b/packages/vscode-ide-companion/resources/webview.html @@ -0,0 +1,2354 @@ + + + + + + + Deep Code + + + +

+ + + + + diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts new file mode 100644 index 00000000..44542e79 --- /dev/null +++ b/packages/vscode-ide-companion/src/extension.ts @@ -0,0 +1,562 @@ +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import OpenAI from "openai"; +import MarkdownIt from "markdown-it"; +import type { SessionMessage } from "@vegamo/deepcode-core"; +import { + SessionManager, + getCompactPromptTokenThreshold, + type LlmStreamProgress, + type PermissionScope, + type SessionEntry, + type SkillInfo, + type UserPromptContent, + type UserToolPermission, + resolveSettingsSources, + type DeepcodingSettings, + type ReasoningEffort, + type ResolvedDeepcodingSettings, + setShellIfWindows, +} from "@vegamo/deepcode-core"; +import { getNonce } from "./utils.js"; +import { handleWebviewMessage } from "./provider.js"; + +const DEFAULT_MODEL = "deepseek-v4-pro"; +const DEFAULT_BASE_URL = "https://api.deepseek.com"; + +type ReasoningMessageParams = { + reasoning_content?: string; +}; + +export class DeepCodeViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = "deepcode.chatView"; + + private readonly context: vscode.ExtensionContext; + private webviewView: vscode.WebviewView | undefined; + private readonly md: MarkdownIt; + private readonly sessionManager: SessionManager; + + constructor(context: vscode.ExtensionContext) { + this.context = context; + this.md = new MarkdownIt({ + html: false, + linkify: false, + breaks: true, + }); + this.sessionManager = new SessionManager({ + projectRoot: this.getWorkspaceRoot(), + createOpenAIClient: () => this.createOpenAIClient(), + getResolvedSettings: () => this.resolveCurrentSettings(), + renderMarkdown: (text) => this.md.render(text), + onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => { + if (!this.webviewView) { + return; + } + if (message.visible === false) { + return; + } + if (message.role !== "tool") { + const reasoningContent = (message.messageParams as ReasoningMessageParams | null)?.reasoning_content; + message.html = this.md.render(message.content || reasoningContent || ""); + } + this.webviewView.webview.postMessage({ type: "appendMessage", message, shouldConnect }); + }, + onSessionEntryUpdated: (entry) => { + if (!this.webviewView) { + return; + } + this.webviewView.webview.postMessage({ + type: "sessionStatus", + sessionId: entry.id, + status: entry.status, + askPermissions: entry.askPermissions, + processes: this.serializeProcesses(entry.processes), + tokenTelemetry: this.buildTokenTelemetry(entry), + }); + }, + onLlmStreamProgress: (progress: LlmStreamProgress) => { + if (!this.webviewView) { + return; + } + this.webviewView.webview.postMessage({ + type: "llmStreamProgress", + progress, + }); + }, + }); + void this.initializeMcpServers(); + } + + dispose(): void { + this.sessionManager.dispose(); + } + + resolveWebviewView(webviewView: vscode.WebviewView): void { + this.webviewView = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this.context.extensionUri], + }; + + webviewView.webview.html = this.getWebviewHtml(webviewView.webview); + + webviewView.webview.onDidReceiveMessage(async (message) => { + const msg = message as Record | undefined; + + // openFile requires vscode API, handle here directly + if (msg?.type === "openFile") { + const filePath = String(msg.filePath || "").trim(); + const line = Number(msg.line || 1); + if (filePath) { + await this.openFileInEditor(filePath, line); + } + return; + } + + const handled = await handleWebviewMessage(message, { + sessionManager: this.sessionManager, + postMessage: (m) => this.webviewView?.webview.postMessage(m), + renderMarkdown: (text) => this.md.render(text), + copyToClipboard: (text) => void vscode.env.clipboard.writeText(text), + }); + + if (!handled) { + // unrecognized message type — no-op + } + }); + } + + private async loadInitialSession(): Promise { + const sessions = this.sessionManager.listSessions(); + const sessionsList = sessions.map((s) => ({ + id: s.id, + summary: s.summary || "Untitled", + createTime: s.createTime, + updateTime: s.updateTime, + status: s.status, + })); + + if (sessions.length === 0) { + // 没有历史会话,显示新对话界面 + this.sendMessage({ + type: "initializeEmpty", + sessions: sessionsList, + status: null, + tokenTelemetry: this.buildTokenTelemetry(null), + }); + return; + } + + // 显示最新的对话 + const latestSession = sessions[0]; + this.loadSession(latestSession.id); + } + + private loadSession(sessionId: string): void { + const session = this.sessionManager.getSession(sessionId); + if (!session) { + return; + } + + // 设置为活动会话 + this.sessionManager.setActiveSessionId(sessionId); + + const messages = this.sessionManager.listSessionMessages(sessionId); + + // 获取所有会话列表 + const sessions = this.sessionManager.listSessions(); + const sessionsList = sessions.map((s) => ({ + id: s.id, + summary: s.summary || "Untitled", + createTime: s.createTime, + updateTime: s.updateTime, + status: s.status, + })); + + // 发送对话信息到 webview + this.sendMessage({ + type: "loadSession", + sessionId, + summary: session.summary || "Untitled", + status: session.status, + askPermissions: session.askPermissions, + processes: this.serializeProcesses(session.processes), + tokenTelemetry: this.buildTokenTelemetry(session), + sessions: sessionsList, + messages: messages + .filter((m) => m.visible) + .map((m) => ({ + role: m.role, + content: m.content, + html: + m.role !== "tool" + ? this.md.render(m.content || (m.messageParams as ReasoningMessageParams | null)?.reasoning_content || "") + : undefined, + meta: m.meta, + })), + }); + } + + private showSessionsList(): void { + const sessions = this.sessionManager.listSessions(); + this.sendMessage({ + type: "showSessionsList", + sessions: sessions.map((s) => ({ + id: s.id, + summary: s.summary || "Untitled", + createTime: s.createTime, + updateTime: s.updateTime, + status: s.status, + })), + }); + } + + private async createNewSession(): Promise { + // 清除当前活动会话 + this.sessionManager.setActiveSessionId(null); + + // 获取所有会话列表 + const sessions = this.sessionManager.listSessions(); + const sessionsList = sessions.map((s) => ({ + id: s.id, + summary: s.summary || "Untitled", + createTime: s.createTime, + updateTime: s.updateTime, + status: s.status, + })); + + this.sendMessage({ + type: "initializeEmpty", + sessions: sessionsList, + status: null, + tokenTelemetry: this.buildTokenTelemetry(null), + }); + await this.sendSkillsList(); + } + + private sendMessage(message: unknown): void { + if (!this.webviewView) { + return; + } + this.webviewView.webview.postMessage(message); + } + + private async sendSkillsList(sessionId?: string): Promise { + if (!this.webviewView) { + return; + } + const skills = await this.sessionManager.listSkills( + sessionId ?? this.sessionManager.getActiveSessionId() ?? undefined + ); + this.sendMessage({ type: "skillsList", skills }); + } + + private async handlePrompt( + prompt: string, + skills?: SkillInfo[], + imageUrls?: string[], + options: { permissions?: UserToolPermission[]; alwaysAllows?: PermissionScope[] } = {} + ): Promise { + if (!this.webviewView) { + return; + } + + const webview = this.webviewView.webview; + const normalizedImages = Array.isArray(imageUrls) ? imageUrls.filter(Boolean) : []; + const displayPrompt = prompt || (normalizedImages.length > 0 ? "粘贴的图像" : ""); + const isPermissionContinue = + prompt === "/continue" && + normalizedImages.length === 0 && + ((options.permissions?.length ?? 0) > 0 || (options.alwaysAllows?.length ?? 0) > 0); + + // 先显示用户消息(原始文本,不做 HTML 格式化) + if (displayPrompt && !isPermissionContinue) { + webview.postMessage({ type: "userMessage", content: displayPrompt }); + } + + webview.postMessage({ type: "loading", value: true }); + + try { + const userPrompt: UserPromptContent = { + text: prompt, + skills, + imageUrls: normalizedImages, + permissions: options.permissions, + alwaysAllows: options.alwaysAllows, + }; + await this.sessionManager.handleUserPrompt(userPrompt); + await this.sendSkillsList(); + + const activeSessionId = this.sessionManager.getActiveSessionId(); + const activeSession = activeSessionId ? this.sessionManager.getSession(activeSessionId) : null; + if (activeSessionId && activeSession) { + webview.postMessage({ + type: "sessionStatus", + sessionId: activeSessionId, + status: activeSession.status, + askPermissions: activeSession.askPermissions, + processes: this.serializeProcesses(activeSession.processes), + tokenTelemetry: this.buildTokenTelemetry(activeSession), + }); + } + + // 发送更新后的会话列表(可能创建了新会话) + const sessions = this.sessionManager.listSessions(); + const sessionsList = sessions.map((s) => ({ + id: s.id, + summary: s.summary || "Untitled", + createTime: s.createTime, + updateTime: s.updateTime, + status: s.status, + })); + webview.postMessage({ + type: "showSessionsList", + sessions: sessionsList, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + webview.postMessage({ + type: "assistant", + html: this.md.render(`Request failed: ${message}`), + }); + } finally { + webview.postMessage({ type: "loading", value: false }); + } + } + + private handlePermissionDenied(sessionId: string): void { + this.sessionManager.denySessionPermission(sessionId); + const session = this.sessionManager.getSession(sessionId); + if (session) { + this.sendMessage({ + type: "sessionStatus", + sessionId, + status: session.status, + askPermissions: session.askPermissions, + processes: this.serializeProcesses(session.processes), + tokenTelemetry: this.buildTokenTelemetry(session), + }); + } + this.showSessionsList(); + } + + private createOpenAIClient(): { + client: OpenAI | null; + model: string; + baseURL: string; + thinkingEnabled: boolean; + reasoningEffort: ReasoningEffort; + debugLogEnabled: boolean; + notify?: string; + webSearchTool?: string; + env?: Record; + machineId?: string; + } { + const settings = this.resolveCurrentSettings(); + + const { apiKey, baseURL, model, thinkingEnabled, reasoningEffort, debugLogEnabled, notify, webSearchTool, env } = + settings; + const machineId = vscode.env.machineId; + + if (!apiKey) { + return { + client: null, + model, + baseURL, + thinkingEnabled, + reasoningEffort, + debugLogEnabled, + notify, + webSearchTool, + env, + machineId, + }; + } + + const client = new OpenAI({ + apiKey, + baseURL: baseURL || undefined, + }); + + return { + client, + model, + baseURL, + thinkingEnabled, + reasoningEffort, + debugLogEnabled, + notify, + webSearchTool, + env, + machineId, + }; + } + + private buildTokenTelemetry(session: SessionEntry | null): { + model: string; + thinkingEnabled: boolean; + reasoningEffort: ReasoningEffort; + activeTokens: number; + compactPromptTokenThreshold: number; + usage: unknown | null; + } { + const settings = this.resolveCurrentSettings(); + return { + model: settings.model, + thinkingEnabled: settings.thinkingEnabled, + reasoningEffort: settings.reasoningEffort, + activeTokens: session?.activeTokens ?? 0, + compactPromptTokenThreshold: getCompactPromptTokenThreshold(settings.model), + usage: session?.usage ?? null, + }; + } + + private async initializeMcpServers(): Promise { + try { + await this.sessionManager.initMcpServers(this.resolveCurrentSettings().mcpServers); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + void vscode.window.showErrorMessage(`Failed to initialize MCP servers: ${message}`); + } + } + + private resolveCurrentSettings(): ResolvedDeepcodingSettings { + return resolveSettingsSources( + this.readUserSettings(), + this.readProjectSettings(), + { + model: DEFAULT_MODEL, + baseURL: DEFAULT_BASE_URL, + }, + process.env + ); + } + + private readUserSettings(): DeepcodingSettings | null { + try { + const settingsPath = path.join(os.homedir(), ".deepcode", "settings.json"); + if (!fs.existsSync(settingsPath)) { + return null; + } + + const raw = fs.readFileSync(settingsPath, "utf8"); + return JSON.parse(raw) as DeepcodingSettings; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`Failed to read ~/.deepcode/settings.json: ${message}`); + return null; + } + } + + private readProjectSettings(): DeepcodingSettings | null { + const workspaceRoot = this.getWorkspaceRoot(); + try { + const settingsPath = path.join(workspaceRoot, ".deepcode", "settings.json"); + if (!fs.existsSync(settingsPath)) { + return null; + } + + const raw = fs.readFileSync(settingsPath, "utf8"); + return JSON.parse(raw) as DeepcodingSettings; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage( + `Failed to read ${path.join(workspaceRoot, ".deepcode", "settings.json")}: ${message}` + ); + return null; + } + } + + private getWorkspaceRoot(): string { + const workspace = vscode.workspace.workspaceFolders?.[0]; + if (workspace) { + return workspace.uri.fsPath; + } + return process.cwd(); + } + + private serializeProcesses( + processes: Map | null + ): Record | null { + if (!processes || processes.size === 0) { + return null; + } + + const serialized: Record = {}; + for (const [pid, entry] of processes.entries()) { + serialized[pid] = entry; + } + return serialized; + } + + private getWebviewHtml(webview: vscode.Webview): string { + const nonce = getNonce(); + const csp = webview.cspSource; + + // 读取 HTML 模板文件 + const htmlPath = vscode.Uri.joinPath(this.context.extensionUri, "resources", "webview.html"); + let html = fs.readFileSync(htmlPath.fsPath, "utf8"); + + // 获取 CSS 文件 URI + const cssPath = vscode.Uri.joinPath(this.context.extensionUri, "resources", "webview.css"); + const cssUri = webview.asWebviewUri(cssPath); + const attachmentsJsPath = vscode.Uri.joinPath(this.context.extensionUri, "resources", "prompt-attachments.js"); + const attachmentsJsUri = webview.asWebviewUri(attachmentsJsPath); + + // 获取 Logo 文件 URI + const iconPath = vscode.Uri.joinPath(this.context.extensionUri, "resources", "deepcoding_icon.png"); + const iconUri = webview.asWebviewUri(iconPath); + + // 替换占位符 + html = html.replace(/\{\{nonce\}\}/g, nonce); + html = html.replace(/\{\{cspSource\}\}/g, csp); + html = html.replace(/\{\{cssUri\}\}/g, cssUri.toString()); + html = html.replace(/\{\{attachmentsJsUri\}\}/g, attachmentsJsUri.toString()); + html = html.replace(/\{\{iconUri\}\}/g, iconUri.toString()); + html = html.replace(/\{\{workspaceRoot\}\}/g, JSON.stringify(this.getWorkspaceRoot())); + + return html; + } + + private async openFileInEditor(filePath: string, line: number): Promise { + const document = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath)); + const editor = await vscode.window.showTextDocument(document, { + preview: false, + preserveFocus: false, + }); + + const targetLine = Number.isFinite(line) && line > 0 ? Math.floor(line) - 1 : 0; + const safeLine = Math.min(Math.max(0, targetLine), Math.max(0, document.lineCount - 1)); + const position = new vscode.Position(safeLine, 0); + const selection = new vscode.Selection(position, position); + editor.selection = selection; + editor.revealRange(new vscode.Range(position, position), vscode.TextEditorRevealType.InCenter); + } +} + +export function activate(context: vscode.ExtensionContext): void { + process.env.NoDefaultCurrentDirectoryInExePath = "1"; + try { + setShellIfWindows(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + void vscode.window.showErrorMessage(message); + } + + const provider = new DeepCodeViewProvider(context); + context.subscriptions.push(provider); + context.subscriptions.push(vscode.window.registerWebviewViewProvider(DeepCodeViewProvider.viewType, provider)); + context.subscriptions.push( + vscode.commands.registerCommand("deepcode.openView", async () => { + await vscode.commands.executeCommand("workbench.view.extension.deepcode"); + await vscode.commands.executeCommand("deepcode.chatView.focus"); + }) + ); +} + +export function deactivate(): void { + // no-op +} diff --git a/packages/vscode-ide-companion/src/provider.ts b/packages/vscode-ide-companion/src/provider.ts new file mode 100644 index 00000000..91aee0e4 --- /dev/null +++ b/packages/vscode-ide-companion/src/provider.ts @@ -0,0 +1,319 @@ +/** + * Message handling logic for the Deepcoding webview provider. + * Extracted from extension.ts for testability — no direct vscode dependency. + */ +import type { SessionManager } from "@vegamo/deepcode-core"; +import type { PermissionScope, SkillInfo, UserToolPermission } from "@vegamo/deepcode-core"; +import { parseUserToolPermissions, parsePermissionScopes } from "./utils.js"; + +export interface PostMessageFn { + (message: unknown): void; +} + +export interface ProviderDeps { + sessionManager: Pick< + SessionManager, + | "listSessions" + | "getSession" + | "getActiveSessionId" + | "setActiveSessionId" + | "listSessionMessages" + | "handleUserPrompt" + | "interruptActiveSession" + | "denySessionPermission" + | "listSkills" + >; + postMessage: PostMessageFn; + renderMarkdown: (text: string) => string; + copyToClipboard: (text: string) => void; +} + +export interface SessionSummary { + id: string; + summary: string; + createTime: string; + updateTime: string; + status: string; +} + +function toSessionList( + sessions: Array<{ id: string; summary?: string | null; createTime: string; updateTime: string; status: string }> +): SessionSummary[] { + return sessions.map((s) => ({ + id: s.id, + summary: s.summary || "Untitled", + createTime: s.createTime, + updateTime: s.updateTime, + status: s.status, + })); +} + +/** + * Routes incoming webview messages to the appropriate handler. + * Returns true if the message was handled. + */ +export async function handleWebviewMessage(message: unknown, deps: ProviderDeps): Promise { + const { sessionManager, postMessage, renderMarkdown, copyToClipboard } = deps; + + if (!message || typeof message !== "object") { + return false; + } + + const msg = message as Record; + + if (msg.type === "ready") { + loadInitialSession(sessionManager, postMessage); + await sendSkillsList(sessionManager, postMessage); + return true; + } + + if (msg.type === "requestSkills") { + await sendSkillsList(sessionManager, postMessage); + return true; + } + + if (msg.type === "userPrompt") { + const prompt = String(msg.prompt || "").trim(); + const images = Array.isArray(msg.images) + ? (msg.images as unknown[]).filter((image): image is string => typeof image === "string" && image.length > 0) + : []; + const permissions = parseUserToolPermissions(msg.permissions); + const alwaysAllows = parsePermissionScopes(msg.alwaysAllows); + if (!prompt && images.length === 0 && permissions.length === 0 && alwaysAllows.length === 0) { + return true; + } + const skills = (msg.skills as SkillInfo[]) || []; + await handlePrompt(prompt, skills, images, sessionManager, postMessage, renderMarkdown, { + permissions: permissions.length > 0 ? permissions : undefined, + alwaysAllows: alwaysAllows.length > 0 ? alwaysAllows : undefined, + }); + return true; + } + + if (msg.type === "interrupt") { + sessionManager.interruptActiveSession(); + return true; + } + + if (msg.type === "denyPermission") { + const sessionId = String(msg.sessionId || sessionManager.getActiveSessionId() || "").trim(); + if (sessionId) { + handlePermissionDenied(sessionId, sessionManager, postMessage); + } + return true; + } + + if (msg.type === "createNewSession") { + await createNewSession(sessionManager, postMessage); + return true; + } + + if (msg.type === "selectSession") { + const sessionId = String(msg.sessionId || "").trim(); + if (sessionId) { + loadSession(sessionId, sessionManager, postMessage, renderMarkdown); + await sendSkillsList(sessionManager, postMessage, sessionId); + } + return true; + } + + if (msg.type === "backToList") { + showSessionsList(sessionManager, postMessage); + return true; + } + + if (msg.type === "openFile") { + // openFile requires vscode API — handled by the caller + return false; + } + + if (msg.type === "copyText") { + const text = String(msg.text || ""); + if (text) { + copyToClipboard(text); + } + return true; + } + + return false; +} + +function loadInitialSession(sessionManager: ProviderDeps["sessionManager"], postMessage: PostMessageFn): void { + const sessions = sessionManager.listSessions(); + const sessionsList = toSessionList(sessions); + + if (sessions.length === 0) { + postMessage({ + type: "initializeEmpty", + sessions: sessionsList, + status: null, + }); + return; + } + + const latestSession = sessions[0]; + loadSession(latestSession.id, sessionManager, postMessage, (t) => t); +} + +export function loadSession( + sessionId: string, + sessionManager: ProviderDeps["sessionManager"], + postMessage: PostMessageFn, + renderMarkdown: (text: string) => string +): void { + const session = sessionManager.getSession(sessionId); + if (!session) { + return; + } + + sessionManager.setActiveSessionId(sessionId); + const messages = sessionManager.listSessionMessages(sessionId); + const sessions = sessionManager.listSessions(); + + postMessage({ + type: "loadSession", + sessionId, + summary: session.summary || "Untitled", + status: session.status, + askPermissions: session.askPermissions, + processes: serializeProcesses(session.processes), + sessions: toSessionList(sessions), + messages: messages + .filter((m) => m.visible) + .map((m) => ({ + role: m.role, + content: m.content, + html: + m.role !== "tool" + ? renderMarkdown( + m.content || (m.messageParams as { reasoning_content?: string } | null)?.reasoning_content || "" + ) + : undefined, + meta: m.meta, + })), + }); +} + +function showSessionsList(sessionManager: ProviderDeps["sessionManager"], postMessage: PostMessageFn): void { + const sessions = sessionManager.listSessions(); + postMessage({ + type: "showSessionsList", + sessions: toSessionList(sessions), + }); +} + +async function createNewSession( + sessionManager: ProviderDeps["sessionManager"], + postMessage: PostMessageFn +): Promise { + sessionManager.setActiveSessionId(null); + const sessions = sessionManager.listSessions(); + + postMessage({ + type: "initializeEmpty", + sessions: toSessionList(sessions), + status: null, + }); + await sendSkillsList(sessionManager, postMessage); +} + +async function sendSkillsList( + sessionManager: ProviderDeps["sessionManager"], + postMessage: PostMessageFn, + sessionId?: string +): Promise { + const skills = await sessionManager.listSkills(sessionId ?? sessionManager.getActiveSessionId() ?? undefined); + postMessage({ type: "skillsList", skills }); +} + +async function handlePrompt( + prompt: string, + skills: SkillInfo[], + imageUrls: string[], + sessionManager: ProviderDeps["sessionManager"], + postMessage: PostMessageFn, + renderMarkdown: (text: string) => string, + options: { permissions?: UserToolPermission[]; alwaysAllows?: PermissionScope[] } = {} +): Promise { + const normalizedImages = imageUrls.filter(Boolean); + const displayPrompt = prompt || (normalizedImages.length > 0 ? "粘贴的图像" : ""); + const isPermissionContinue = + prompt === "/continue" && + normalizedImages.length === 0 && + ((options.permissions?.length ?? 0) > 0 || (options.alwaysAllows?.length ?? 0) > 0); + + if (displayPrompt && !isPermissionContinue) { + postMessage({ type: "userMessage", content: displayPrompt }); + } + + postMessage({ type: "loading", value: true }); + + try { + await sessionManager.handleUserPrompt({ + text: prompt, + skills, + imageUrls: normalizedImages, + permissions: options.permissions, + alwaysAllows: options.alwaysAllows, + }); + await sendSkillsList(sessionManager, postMessage); + + const activeSessionId = sessionManager.getActiveSessionId(); + const activeSession = activeSessionId ? sessionManager.getSession(activeSessionId) : null; + if (activeSessionId && activeSession) { + postMessage({ + type: "sessionStatus", + sessionId: activeSessionId, + status: activeSession.status, + askPermissions: activeSession.askPermissions, + processes: serializeProcesses(activeSession.processes), + }); + } + + const sessions = sessionManager.listSessions(); + postMessage({ + type: "showSessionsList", + sessions: toSessionList(sessions), + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + postMessage({ + type: "assistant", + html: renderMarkdown(`Request failed: ${message}`), + }); + } finally { + postMessage({ type: "loading", value: false }); + } +} + +function handlePermissionDenied( + sessionId: string, + sessionManager: ProviderDeps["sessionManager"], + postMessage: PostMessageFn +): void { + sessionManager.denySessionPermission(sessionId); + const session = sessionManager.getSession(sessionId); + if (session) { + postMessage({ + type: "sessionStatus", + sessionId, + status: session.status, + askPermissions: session.askPermissions, + processes: serializeProcesses(session.processes), + }); + } + showSessionsList(sessionManager, postMessage); +} + +function serializeProcesses( + processes: Map | null +): Record | null { + if (!processes || processes.size === 0) { + return null; + } + const serialized: Record = {}; + for (const [pid, entry] of processes.entries()) { + serialized[pid] = entry; + } + return serialized; +} diff --git a/packages/vscode-ide-companion/src/tests/extension-utils.test.ts b/packages/vscode-ide-companion/src/tests/extension-utils.test.ts new file mode 100644 index 00000000..8dea36de --- /dev/null +++ b/packages/vscode-ide-companion/src/tests/extension-utils.test.ts @@ -0,0 +1,132 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { VALID_PERMISSION_SCOPES, parseUserToolPermissions, parsePermissionScopes, getNonce } from "../utils.js"; + +// --- VALID_PERMISSION_SCOPES --- + +test("VALID_PERMISSION_SCOPES contains all expected scopes", () => { + const expected = [ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "mcp", + ]; + assert.equal(VALID_PERMISSION_SCOPES.size, expected.length); + for (const scope of expected) { + assert.ok(VALID_PERMISSION_SCOPES.has(scope as any), `missing scope: ${scope}`); + } +}); + +// --- parseUserToolPermissions --- + +test("parseUserToolPermissions returns empty array for non-array input", () => { + assert.deepEqual(parseUserToolPermissions(undefined), []); + assert.deepEqual(parseUserToolPermissions(null), []); + assert.deepEqual(parseUserToolPermissions("string"), []); + assert.deepEqual(parseUserToolPermissions(123), []); + assert.deepEqual(parseUserToolPermissions({}), []); +}); + +test("parseUserToolPermissions returns empty array for empty array", () => { + assert.deepEqual(parseUserToolPermissions([]), []); +}); + +test("parseUserToolPermissions parses valid permissions", () => { + const input = [ + { toolCallId: "call-1", permission: "allow" }, + { toolCallId: "call-2", permission: "deny" }, + ]; + const result = parseUserToolPermissions(input); + assert.equal(result.length, 2); + assert.deepEqual(result[0], { toolCallId: "call-1", permission: "allow" }); + assert.deepEqual(result[1], { toolCallId: "call-2", permission: "deny" }); +}); + +test("parseUserToolPermissions filters out invalid items", () => { + const input = [ + null, + 123, + "string", + {}, + { toolCallId: "", permission: "allow" }, // empty toolCallId + { toolCallId: " ", permission: "allow" }, // whitespace-only toolCallId + { toolCallId: "call-1" }, // missing permission + { toolCallId: "call-2", permission: "invalid" }, // invalid permission value + { permission: "allow" }, // missing toolCallId + { toolCallId: "call-3", permission: "allow" }, // valid + ]; + const result = parseUserToolPermissions(input); + assert.equal(result.length, 1); + assert.deepEqual(result[0], { toolCallId: "call-3", permission: "allow" }); +}); + +test("parseUserToolPermissions preserves toolCallId with leading/trailing spaces", () => { + const input = [{ toolCallId: " call-1 ", permission: "allow" }]; + const result = parseUserToolPermissions(input); + // trimmed toolCallId " " fails the .trim() check, so this item is filtered + // Wait, " call-1 ".trim() = "call-1" which is truthy, so it passes + assert.equal(result.length, 1); + assert.equal(result[0].toolCallId, " call-1 "); +}); + +// --- parsePermissionScopes --- + +test("parsePermissionScopes returns empty array for non-array input", () => { + assert.deepEqual(parsePermissionScopes(undefined), []); + assert.deepEqual(parsePermissionScopes(null), []); + assert.deepEqual(parsePermissionScopes("string"), []); + assert.deepEqual(parsePermissionScopes(123), []); + assert.deepEqual(parsePermissionScopes({}), []); +}); + +test("parsePermissionScopes returns empty array for empty array", () => { + assert.deepEqual(parsePermissionScopes([]), []); +}); + +test("parsePermissionScopes parses valid scopes", () => { + const input = ["read-in-cwd", "write-in-cwd", "network"]; + const result = parsePermissionScopes(input); + assert.equal(result.length, 3); + assert.deepEqual(result, ["read-in-cwd", "write-in-cwd", "network"]); +}); + +test("parsePermissionScopes filters out invalid values", () => { + const input = ["read-in-cwd", "invalid-scope", 123, null, undefined, {}, "mcp"]; + const result = parsePermissionScopes(input); + assert.equal(result.length, 2); + assert.deepEqual(result, ["read-in-cwd", "mcp"]); +}); + +test("parsePermissionScopes deduplicates scopes", () => { + const input = ["read-in-cwd", "write-in-cwd", "read-in-cwd", "network", "network"]; + const result = parsePermissionScopes(input); + assert.equal(result.length, 3); + assert.deepEqual(result, ["read-in-cwd", "write-in-cwd", "network"]); +}); + +// --- getNonce --- + +test("getNonce returns a 32-character string", () => { + const nonce = getNonce(); + assert.equal(nonce.length, 32); +}); + +test("getNonce only contains alphanumeric characters", () => { + const nonce = getNonce(); + assert.ok(/^[A-Za-z0-9]+$/.test(nonce), `nonce contains non-alphanumeric chars: ${nonce}`); +}); + +test("getNonce returns different values on successive calls", () => { + const nonces = new Set(); + for (let i = 0; i < 100; i++) { + nonces.add(getNonce()); + } + // With 62^32 possible values, 100 calls should almost certainly be unique + assert.ok(nonces.size > 90, `Expected mostly unique nonces, got ${nonces.size} unique out of 100`); +}); diff --git a/packages/vscode-ide-companion/src/tests/extension.test.ts b/packages/vscode-ide-companion/src/tests/extension.test.ts new file mode 100644 index 00000000..4f8d6e03 --- /dev/null +++ b/packages/vscode-ide-companion/src/tests/extension.test.ts @@ -0,0 +1,445 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { handleWebviewMessage, loadSession, type ProviderDeps } from "../provider.js"; + +// --- Helpers --- + +function createMockSessionManager(options?: { sessions?: any[]; messages?: any[]; skills?: any[] }) { + const sessions = options?.sessions ?? [ + { + id: "session-1", + summary: "Test Session", + status: "idle", + askPermissions: null, + processes: null, + activeTokens: 100, + usage: null, + createTime: "2025-01-01T00:00:00Z", + updateTime: "2025-01-01T00:00:00Z", + }, + ]; + const messages = options?.messages ?? []; + const skills = options?.skills ?? []; + let activeSessionId: string | null = sessions[0]?.id ?? null; + + return { + dispose: () => {}, + listSessions: () => sessions, + getSession: (id: string) => sessions.find((s: any) => s.id === id) ?? null, + getActiveSessionId: () => activeSessionId, + setActiveSessionId: (id: string | null) => { + activeSessionId = id; + }, + listSessionMessages: (_sessionId: string) => messages, + handleUserPrompt: () => Promise.resolve(), + interruptActiveSession: () => {}, + denySessionPermission: (_sessionId: string) => {}, + listSkills: () => Promise.resolve(skills), + initMcpServers: () => Promise.resolve(), + }; +} + +function createDeps(options?: Parameters[0]): ProviderDeps & { messages: unknown[] } { + const messages: unknown[] = []; + return { + sessionManager: createMockSessionManager(options), + postMessage: (msg: unknown) => { + messages.push(msg); + }, + renderMarkdown: (text: string) => `

${text}

`, + copyToClipboard: () => {}, + messages, + }; +} + +// --- handleWebviewMessage routing --- + +test("handleWebviewMessage returns false for null message", async () => { + const deps = createDeps(); + const result = await handleWebviewMessage(null, deps); + assert.equal(result, false); +}); + +test("handleWebviewMessage returns false for non-object message", async () => { + const deps = createDeps(); + assert.equal(await handleWebviewMessage("string", deps), false); + assert.equal(await handleWebviewMessage(123, deps), false); +}); + +test("handleWebviewMessage returns false for unknown message type", async () => { + const deps = createDeps(); + assert.equal(await handleWebviewMessage({ type: "unknownType" }, deps), false); +}); + +test("ready message triggers loadInitialSession and sendSkillsList", async () => { + const deps = createDeps(); + const handled = await handleWebviewMessage({ type: "ready" }, deps); + + assert.equal(handled, true); + const types = deps.messages.map((m: any) => m.type); + // With sessions present, should send loadSession + skillsList + assert.ok(types.includes("loadSession"), `Expected loadSession, got: ${types.join(", ")}`); + assert.ok(types.includes("skillsList"), `Expected skillsList, got: ${types.join(", ")}`); +}); + +test("ready with no sessions sends initializeEmpty", async () => { + const deps = createDeps({ sessions: [] }); + await handleWebviewMessage({ type: "ready" }, deps); + + const types = deps.messages.map((m: any) => m.type); + assert.ok(types.includes("initializeEmpty"), `Expected initializeEmpty, got: ${types.join(", ")}`); +}); + +test("requestSkills sends skillsList", async () => { + const deps = createDeps({ skills: [{ name: "test-skill" }] }); + await handleWebviewMessage({ type: "requestSkills" }, deps); + + const skillsMsg = deps.messages.find((m: any) => m.type === "skillsList"); + assert.ok(skillsMsg, "Should send skillsList"); + assert.deepEqual((skillsMsg as any).skills, [{ name: "test-skill" }]); +}); + +test("interrupt calls interruptActiveSession", async () => { + const deps = createDeps(); + let interrupted = false; + (deps.sessionManager as any).interruptActiveSession = () => { + interrupted = true; + }; + + const handled = await handleWebviewMessage({ type: "interrupt" }, deps); + assert.equal(handled, true); + assert.ok(interrupted, "interruptActiveSession should be called"); +}); + +test("createNewSession clears active session and sends initializeEmpty", async () => { + const deps = createDeps(); + let cleared = false; + (deps.sessionManager as any).setActiveSessionId = (id: string | null) => { + if (id === null) cleared = true; + }; + + await handleWebviewMessage({ type: "createNewSession" }, deps); + + assert.ok(cleared, "setActiveSessionId(null) should be called"); + const types = deps.messages.map((m: any) => m.type); + assert.ok(types.includes("initializeEmpty"), `Expected initializeEmpty, got: ${types.join(", ")}`); + assert.ok(types.includes("skillsList"), `Expected skillsList, got: ${types.join(", ")}`); +}); + +test("selectSession loads session and sends skillsList", async () => { + const deps = createDeps(); + let loadedId: string | null = null; + (deps.sessionManager as any).setActiveSessionId = (id: string) => { + loadedId = id; + }; + + await handleWebviewMessage({ type: "selectSession", sessionId: "session-1" }, deps); + + assert.equal(loadedId, "session-1"); + const types = deps.messages.map((m: any) => m.type); + assert.ok(types.includes("loadSession"), `Expected loadSession, got: ${types.join(", ")}`); + assert.ok(types.includes("skillsList"), `Expected skillsList, got: ${types.join(", ")}`); +}); + +test("selectSession with empty sessionId does nothing", async () => { + const deps = createDeps(); + await handleWebviewMessage({ type: "selectSession", sessionId: "" }, deps); + assert.equal(deps.messages.length, 0, "No messages for empty sessionId"); +}); + +test("selectSession with non-existent session does not send loadSession", async () => { + const deps = createDeps(); + (deps.sessionManager as any).getSession = () => null; + + await handleWebviewMessage({ type: "selectSession", sessionId: "non-existent" }, deps); + + const types = deps.messages.map((m: any) => m.type); + assert.ok(!types.includes("loadSession"), "Should not send loadSession for non-existent session"); +}); + +test("backToList sends showSessionsList", async () => { + const deps = createDeps(); + await handleWebviewMessage({ type: "backToList" }, deps); + + const msg = deps.messages.find((m: any) => m.type === "showSessionsList"); + assert.ok(msg, "Should send showSessionsList"); + assert.ok(Array.isArray((msg as any).sessions), "sessions should be an array"); +}); + +test("denyPermission calls denySessionPermission and sends sessionStatus", async () => { + const deps = createDeps(); + let deniedId: string | null = null; + (deps.sessionManager as any).denySessionPermission = (id: string) => { + deniedId = id; + }; + + await handleWebviewMessage({ type: "denyPermission", sessionId: "session-1" }, deps); + + assert.equal(deniedId, "session-1"); + const types = deps.messages.map((m: any) => m.type); + assert.ok(types.includes("sessionStatus"), `Expected sessionStatus, got: ${types.join(", ")}`); + assert.ok(types.includes("showSessionsList"), `Expected showSessionsList, got: ${types.join(", ")}`); +}); + +test("denyPermission with empty sessionId does nothing", async () => { + const deps = createDeps(); + (deps.sessionManager as any).getActiveSessionId = () => null; + + await handleWebviewMessage({ type: "denyPermission", sessionId: "" }, deps); + + // No sessionStatus should be sent + const types = deps.messages.map((m: any) => m.type); + assert.ok(!types.includes("sessionStatus"), "Should not send sessionStatus for empty sessionId"); +}); + +test("copyText calls copyToClipboard", async () => { + const deps = createDeps(); + let copiedText: string | null = null; + deps.copyToClipboard = (text: string) => { + copiedText = text; + }; + + const handled = await handleWebviewMessage({ type: "copyText", text: "hello" }, deps); + assert.equal(handled, true); + assert.equal(copiedText, "hello"); +}); + +test("copyText with empty text does not call copyToClipboard", async () => { + const deps = createDeps(); + let copied = false; + deps.copyToClipboard = () => { + copied = true; + }; + + await handleWebviewMessage({ type: "copyText", text: "" }, deps); + assert.ok(!copied, "Should not copy empty text"); +}); + +test("openFile returns false (handled by caller)", async () => { + const deps = createDeps(); + const result = await handleWebviewMessage({ type: "openFile", filePath: "/some/file.ts" }, deps); + assert.equal(result, false); +}); + +// --- userPrompt --- + +test("userPrompt with empty prompt and no images/permissions is handled without messages", async () => { + const deps = createDeps(); + const handled = await handleWebviewMessage( + { type: "userPrompt", prompt: "", images: [], permissions: [], alwaysAllows: [] }, + deps + ); + assert.equal(handled, true); + assert.equal(deps.messages.length, 0, "No messages for empty prompt"); +}); + +test("userPrompt with text sends userMessage and loading states", async () => { + const deps = createDeps(); + await handleWebviewMessage({ type: "userPrompt", prompt: "hello" }, deps); + + const types = deps.messages.map((m: any) => m.type); + assert.ok(types.includes("userMessage"), `Expected userMessage, got: ${types.join(", ")}`); + assert.ok(types.includes("loading"), `Expected loading, got: ${types.join(", ")}`); + + // Should end with loading: false + const lastLoading = [...deps.messages].reverse().find((m: any) => m.type === "loading"); + assert.deepEqual(lastLoading, { type: "loading", value: false }); +}); + +test("userPrompt with images sends userMessage with image placeholder", async () => { + const deps = createDeps(); + await handleWebviewMessage({ type: "userPrompt", prompt: "", images: ["data:image/png;base64,abc"] }, deps); + + const userMsg = deps.messages.find((m: any) => m.type === "userMessage"); + assert.ok(userMsg, "Should send userMessage for images"); + assert.equal((userMsg as any).content, "粘贴的图像"); +}); + +test("userPrompt with permissions (continue) does not send userMessage", async () => { + const deps = createDeps(); + await handleWebviewMessage( + { + type: "userPrompt", + prompt: "/continue", + images: [], + permissions: [{ toolCallId: "call-1", permission: "allow" }], + }, + deps + ); + + const userMsg = deps.messages.find((m: any) => m.type === "userMessage"); + assert.ok(!userMsg, "Should not send userMessage for /continue with permissions"); +}); + +test("userPrompt sends sessionStatus after handling", async () => { + const deps = createDeps(); + await handleWebviewMessage({ type: "userPrompt", prompt: "hello" }, deps); + + const types = deps.messages.map((m: any) => m.type); + assert.ok(types.includes("sessionStatus"), `Expected sessionStatus, got: ${types.join(", ")}`); +}); + +test("userPrompt sends showSessionsList after handling", async () => { + const deps = createDeps(); + await handleWebviewMessage({ type: "userPrompt", prompt: "hello" }, deps); + + const sessionsMsg = deps.messages.find((m: any) => m.type === "showSessionsList"); + assert.ok(sessionsMsg, "Should send showSessionsList"); + assert.ok(Array.isArray((sessionsMsg as any).sessions), "sessions should be an array"); +}); + +test("userPrompt on error sends assistant error message", async () => { + const deps = createDeps(); + (deps.sessionManager as any).handleUserPrompt = () => Promise.reject(new Error("API failed")); + + await handleWebviewMessage({ type: "userPrompt", prompt: "hello" }, deps); + + const assistantMsg = deps.messages.find((m: any) => m.type === "assistant"); + assert.ok(assistantMsg, "Should send assistant error message"); + assert.ok((assistantMsg as any).html.includes("API failed"), "Error message should contain the error text"); +}); + +test("userPrompt always sends loading: false even on error", async () => { + const deps = createDeps(); + (deps.sessionManager as any).handleUserPrompt = () => Promise.reject(new Error("fail")); + + await handleWebviewMessage({ type: "userPrompt", prompt: "hello" }, deps); + + const lastLoading = [...deps.messages].reverse().find((m: any) => m.type === "loading"); + assert.deepEqual(lastLoading, { type: "loading", value: false }); +}); + +// --- loadSession --- + +test("loadSession sends loadSession with correct fields", () => { + const sessionManager = createMockSessionManager(); + const messages: unknown[] = []; + const postMessage = (msg: unknown) => { + messages.push(msg); + }; + + loadSession("session-1", sessionManager, postMessage, (t) => t); + + const msg = messages.find((m: any) => m.type === "loadSession") as any; + assert.ok(msg, "Should send loadSession"); + assert.equal(msg.sessionId, "session-1"); + assert.equal(msg.summary, "Test Session"); + assert.equal(msg.status, "idle"); + assert.ok(Array.isArray(msg.sessions), "sessions should be an array"); + assert.ok(Array.isArray(msg.messages), "messages should be an array"); +}); + +test("loadSession with non-existent session does nothing", () => { + const sessionManager = createMockSessionManager(); + const messages: unknown[] = []; + const postMessage = (msg: unknown) => { + messages.push(msg); + }; + + (sessionManager as any).getSession = () => null; + loadSession("non-existent", sessionManager, postMessage, (t) => t); + + assert.equal(messages.length, 0, "No messages for non-existent session"); +}); + +test("loadSession sets active session id", () => { + const sessionManager = createMockSessionManager(); + const messages: unknown[] = []; + let setTo: string | null = null; + (sessionManager as any).setActiveSessionId = (id: string) => { + setTo = id; + }; + + loadSession( + "session-1", + sessionManager, + (msg) => messages.push(msg), + (t) => t + ); + + assert.equal(setTo, "session-1"); +}); + +test("loadSession filters out invisible messages", () => { + const sessionManager = createMockSessionManager({ + messages: [ + { role: "user", content: "visible", visible: true }, + { role: "assistant", content: "hidden", visible: false }, + { role: "user", content: "also visible", visible: true }, + ], + }); + const messages: unknown[] = []; + loadSession( + "session-1", + sessionManager, + (msg) => messages.push(msg), + (t) => t + ); + + const loadMsg = messages.find((m: any) => m.type === "loadSession") as any; + assert.equal(loadMsg.messages.length, 2, "Should filter out invisible messages"); +}); + +// --- serializeProcesses --- + +test("loadSession serializes processes map to object", () => { + const sessionManager = createMockSessionManager({ + sessions: [ + { + id: "session-1", + summary: "Test", + status: "idle", + askPermissions: null, + processes: new Map([ + ["123", { startTime: "2025-01-01", command: "ls" }], + ["456", { startTime: "2025-01-02", command: "cat" }], + ]), + activeTokens: 0, + usage: null, + createTime: "2025-01-01", + updateTime: "2025-01-01", + }, + ], + }); + const messages: unknown[] = []; + loadSession( + "session-1", + sessionManager, + (msg) => messages.push(msg), + (t) => t + ); + + const loadMsg = messages.find((m: any) => m.type === "loadSession") as any; + assert.deepEqual(loadMsg.processes, { + "123": { startTime: "2025-01-01", command: "ls" }, + "456": { startTime: "2025-01-02", command: "cat" }, + }); +}); + +test("loadSession returns null for empty processes", () => { + const sessionManager = createMockSessionManager({ + sessions: [ + { + id: "session-1", + summary: "Test", + status: "idle", + askPermissions: null, + processes: null, + activeTokens: 0, + usage: null, + createTime: "2025-01-01", + updateTime: "2025-01-01", + }, + ], + }); + const messages: unknown[] = []; + loadSession( + "session-1", + sessionManager, + (msg) => messages.push(msg), + (t) => t + ); + + const loadMsg = messages.find((m: any) => m.type === "loadSession") as any; + assert.equal(loadMsg.processes, null); +}); diff --git a/packages/vscode-ide-companion/src/tests/run-tests.mjs b/packages/vscode-ide-companion/src/tests/run-tests.mjs new file mode 100644 index 00000000..73034d8a --- /dev/null +++ b/packages/vscode-ide-companion/src/tests/run-tests.mjs @@ -0,0 +1,15 @@ +// Test runner for @vegamo/deepcode-vscode +import { globSync } from "glob"; +import { spawnSync } from "child_process"; +import { fileURLToPath } from "url"; +import * as path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const testFiles = globSync("*.test.ts", { cwd: __dirname }); + +const result = spawnSync(process.execPath, ["--import", "tsx", "--test", ...testFiles], { + stdio: "inherit", + cwd: __dirname, +}); + +process.exit(result.status ?? 1); diff --git a/packages/vscode-ide-companion/src/utils.ts b/packages/vscode-ide-companion/src/utils.ts new file mode 100644 index 00000000..7fc126e8 --- /dev/null +++ b/packages/vscode-ide-companion/src/utils.ts @@ -0,0 +1,61 @@ +import type { PermissionScope, UserToolPermission } from "@vegamo/deepcode-core"; + +export const VALID_PERMISSION_SCOPES = new Set([ + "read-in-cwd", + "read-out-cwd", + "write-in-cwd", + "write-out-cwd", + "delete-in-cwd", + "delete-out-cwd", + "query-git-log", + "mutate-git-log", + "network", + "mcp", +]); + +export function parseUserToolPermissions(value: unknown): UserToolPermission[] { + if (!Array.isArray(value)) { + return []; + } + const result: UserToolPermission[] = []; + for (const item of value) { + if (!item || typeof item !== "object") { + continue; + } + const record = item as { toolCallId?: unknown; permission?: unknown }; + if (typeof record.toolCallId !== "string" || !record.toolCallId.trim()) { + continue; + } + if (record.permission !== "allow" && record.permission !== "deny") { + continue; + } + result.push({ toolCallId: record.toolCallId, permission: record.permission }); + } + return result; +} + +export function parsePermissionScopes(value: unknown): PermissionScope[] { + if (!Array.isArray(value)) { + return []; + } + const result: PermissionScope[] = []; + for (const item of value) { + if (typeof item !== "string" || !VALID_PERMISSION_SCOPES.has(item as PermissionScope)) { + continue; + } + const scope = item as PermissionScope; + if (!result.includes(scope)) { + result.push(scope); + } + } + return result; +} + +export function getNonce(): string { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i += 1) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/packages/vscode-ide-companion/tsconfig.build.json b/packages/vscode-ide-companion/tsconfig.build.json new file mode 100644 index 00000000..8601700a --- /dev/null +++ b/packages/vscode-ide-companion/tsconfig.build.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "ignoreDeprecations": "6.0", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "composite": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "types": ["node", "vscode"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/vscode-ide-companion/tsconfig.json b/packages/vscode-ide-companion/tsconfig.json new file mode 100644 index 00000000..c0d84433 --- /dev/null +++ b/packages/vscode-ide-companion/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "ignoreDeprecations": "6.0", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node", "vscode"], + "baseUrl": ".", + "paths": { + "@vegamo/deepcode-core": ["../core/src/index.ts"], + "@vegamo/deepcode-core/*": ["../core/src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", ".vscode-test", "out"] +} diff --git a/scripts/build-vscode-companion.js b/scripts/build-vscode-companion.js new file mode 100644 index 00000000..7288c786 --- /dev/null +++ b/scripts/build-vscode-companion.js @@ -0,0 +1,23 @@ +import { spawnSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); + +function run(command, args, label) { + console.log(`\n[${label}] ${command} ${args.join(" ")}`); + const result = spawnSync(command, args, { stdio: "inherit", cwd: root, shell: true }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +console.log("========================================="); +console.log(" Deep Code — Build VSCode Companion"); +console.log("========================================="); + +run("npm", ["run", "build", "--workspace=@vegamo/deepcode-core"], "1/3 Build core"); +run("node", ["scripts/esbuild-vscode.config.js"], "2/3 Bundle extension"); +run("npm", ["run", "package", "--workspace=deepcode-vscode"], "3/3 Package .vsix"); + +console.log("\n✅ VSCode companion build complete.\n\n"); diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 00000000..080e2e54 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,23 @@ +import { spawnSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); + +function run(command, args, label) { + process.stdout.write(`\n[${label}] ${command} ${args.join(" ")}\n`); + const result = spawnSync(command, args, { stdio: "inherit", cwd: root, shell: true }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +console.log("========================================="); +console.log(" Deep Code CLI — Build"); +console.log("========================================="); + +run("npm", ["run", "build", "--workspace=@vegamo/deepcode-core"], "1/2"); +run("npm", ["run", "bundle"], "2/2"); + +console.log("\n✅ Build complete.\n\n"); diff --git a/scripts/clean.js b/scripts/clean.js new file mode 100644 index 00000000..db532c3b --- /dev/null +++ b/scripts/clean.js @@ -0,0 +1,39 @@ +import { rmSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { globSync } from "glob"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); +const RMRF = { recursive: true, force: true }; + +console.log("Cleaning build artifacts...\n"); + +// Root artifacts +rmSync(join(root, "node_modules"), RMRF); +console.log(" rm node_modules/"); + +// Generated version files +for (const pkg of ["cli", "core", "vscode-ide-companion"]) { + rmSync(join(root, "packages", pkg, "src", "generated"), RMRF); + console.log(` rm packages/${pkg}/src/generated/`); +} + +// All workspace dist/ and tsbuildinfo +const packageDirs = globSync("packages/*", { cwd: root, absolute: true }); +for (const pkgDir of packageDirs) { + rmSync(join(pkgDir, "dist"), RMRF); + console.log(` rm ${join(pkgDir, "dist")}`); + rmSync(join(pkgDir, "tsconfig.tsbuildinfo"), { force: true }); +} + +// Clean up vscode-ide-companion package +rmSync(join(root, "packages/vscode-ide-companion/node_modules"), RMRF); +// VSCode .vsix files +const vsixFiles = globSync("packages/vscode-ide-companion/*.vsix", { cwd: root }); +for (const vsixFile of vsixFiles) { + rmSync(join(root, vsixFile), RMRF); + console.log(` rm ${vsixFile}`); +} + +console.log("\n✅ Clean complete.\n\n"); diff --git a/scripts/copy_bundle_assets.js b/scripts/copy-bundle-assets.js similarity index 62% rename from scripts/copy_bundle_assets.js rename to scripts/copy-bundle-assets.js index 0e1dd948..88315484 100644 --- a/scripts/copy_bundle_assets.js +++ b/scripts/copy-bundle-assets.js @@ -2,12 +2,11 @@ import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -/* global console, process */ - const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, ".."); -const distDir = join(root, "dist"); -const bundledSkillsSrc = join(root, "templates", "skills", "bundled"); +const cliRoot = join(root, "packages", "cli"); +const distDir = join(cliRoot, "dist"); +const bundledSkillsSrc = join(root, "packages", "core", "templates", "skills", "bundled"); const bundledSkillsDest = join(distDir, "bundled"); if (!existsSync(distDir)) { @@ -15,8 +14,8 @@ if (!existsSync(distDir)) { } if (!existsSync(bundledSkillsSrc)) { - console.warn(`Bundled skills directory not found at ${bundledSkillsSrc}`); - process.exit(0); + console.error(`Bundled skills directory not found at ${bundledSkillsSrc}`); + process.exit(1); } rmSync(bundledSkillsDest, { recursive: true, force: true }); @@ -24,4 +23,5 @@ cpSync(bundledSkillsSrc, bundledSkillsDest, { recursive: true, dereference: true, }); -console.log("Copied bundled built-in skills to dist/bundled/"); + +console.log("\n✅ All bundle assets copied to dist/bundled/"); diff --git a/scripts/esbuild-vscode.config.js b/scripts/esbuild-vscode.config.js new file mode 100644 index 00000000..25cd2bc2 --- /dev/null +++ b/scripts/esbuild-vscode.config.js @@ -0,0 +1,29 @@ +import { build } from "esbuild"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); + +const vscodeRoot = join(root, "packages", "vscode-ide-companion"); +const entry = join(vscodeRoot, "src", "extension.ts"); +const outfile = join(vscodeRoot, "out", "extension.js"); + +await build({ + entryPoints: [entry], + bundle: true, + platform: "node", + format: "cjs", + target: "node18", + outfile, + external: ["vscode"], + sourcemap: true, + footer: { + js: "module.exports = { activate, deactivate };", + }, + logOverride: { + "empty-import-meta": "silent", + }, +}); + +console.log(`\n✅ ${outfile} built successfully\n\n`); diff --git a/scripts/esbuild.config.js b/scripts/esbuild.config.js new file mode 100644 index 00000000..bf814a32 --- /dev/null +++ b/scripts/esbuild.config.js @@ -0,0 +1,28 @@ +import { build } from "esbuild"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); + +const cliRoot = join(root, "packages", "cli"); +const entry = join(cliRoot, "src", "cli.tsx"); +const outfile = join(cliRoot, "dist", "cli.js"); + +await build({ + entryPoints: [entry], + bundle: true, + platform: "node", + format: "esm", + target: "node22", + outfile, + banner: { js: "#!/usr/bin/env node" }, + jsx: "automatic", + jsxImportSource: "react", + packages: "external", + logOverride: { + "empty-import-meta": "silent", + }, +}); + +console.log(`\n✅ ${outfile} built successfully\n\n`); diff --git a/scripts/generate-git-commit-info.js b/scripts/generate-git-commit-info.js new file mode 100644 index 00000000..ac029533 --- /dev/null +++ b/scripts/generate-git-commit-info.js @@ -0,0 +1,46 @@ +import { execSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); +const scriptPath = relative(root, fileURLToPath(import.meta.url)); + +const generatedCliDir = join(root, "packages", "cli", "src", "generated"); +const cliGitCommitFile = join(generatedCliDir, "git-commit.ts"); + +let gitCommitInfo = "N/A"; +let cliVersion = "UNKNOWN"; + +if (!existsSync(generatedCliDir)) { + mkdirSync(generatedCliDir, { recursive: true }); +} + +try { + const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim(); + if (gitHash) { + gitCommitInfo = gitHash; + } + + const pkg = JSON.parse(readFileSync(join(root, "packages", "cli", "package.json"), "utf-8")); + cliVersion = pkg.version ?? "UNKNOWN"; +} catch { + // ignore +} + +const fileContent = [ + "/**", + " * @license", + ` * Copyright ${new Date().getFullYear()} @vegamo deepcode`, + " */", + "", + `// Auto-generated by ${scriptPath}. Do not edit.`, + `export const GIT_COMMIT_INFO = "${gitCommitInfo}";`, + `export const CLI_VERSION = "${cliVersion}";`, + "", +].join("\n"); + +writeFileSync(cliGitCommitFile, fileContent); + +console.log(`Generated version info: ${cliVersion} (${gitCommitInfo})`); diff --git a/scripts/start.js b/scripts/start.js new file mode 100644 index 00000000..b3d83be1 --- /dev/null +++ b/scripts/start.js @@ -0,0 +1,22 @@ +import { existsSync } from "node:fs"; +import { spawn } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); +const cliDist = join(root, "packages", "cli", "dist", "cli.js"); + +if (!existsSync(cliDist)) { + console.error(`Error: ${cliDist} not found. Run 'npm run build' first.`); + process.exit(1); +} + +console.log("Starting Deep Code CLI...\n"); + +const child = spawn("node", [cliDist, ...process.argv.slice(2)], { + stdio: "inherit", + cwd: root, +}); + +child.on("exit", (code) => process.exit(code ?? 1)); diff --git a/src/tests/run-tests.mjs b/src/tests/run-tests.mjs deleted file mode 100644 index 4d09f5b5..00000000 --- a/src/tests/run-tests.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// Cross-platform test runner: finds all *.test.ts files and runs them via tsx. -// Uses the glob package for reliable cross-platform pattern expansion (Node 20+). -/* eslint-disable */ - -import { globSync } from "glob"; -import { spawnSync } from "child_process"; - -const cwd = new URL("../..", import.meta.url); -const testFiles = globSync("src/tests/*.test.ts", { cwd }); - -const result = spawnSync(process.execPath, ["--import", "tsx", "--test", ...testFiles], { stdio: "inherit", cwd }); - -process.exit(result.status ?? 1); diff --git a/tsconfig.json b/tsconfig.json index 24a6a1d1..9076eecf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,38 @@ { "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "ignoreDeprecations": "6.0", - "lib": ["ES2022"], - "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitThis": true, "forceConsistentCasingInFileNames": true, + "noPropertyAccessFromIndexSignature": true, + "noUnusedLocals": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, "resolveJsonModule": true, - "noEmit": true, - "isolatedModules": true, + "sourceMap": true, + "composite": true, + "incremental": true, + "declaration": true, "allowSyntheticDefaultImports": true, - "types": ["node"] + "verbatimModuleSyntax": true, + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "bundler", + "target": "ES2022", + "types": ["node"], + "jsx": "react-jsx" }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist"] + "include": [], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/cli" }, + { "path": "./packages/vscode-ide-companion" } + ] } From b4d89efbf43f519a46689873ea99590731ec7e62 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 16 Jun 2026 14:32:46 +0800 Subject: [PATCH 165/212] fix: update package-lock.json to sync with workspace name change --- package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e6afcbc..f63d16b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1964,10 +1964,6 @@ "resolved": "packages/core", "link": true }, - "node_modules/@vegamo/deepcode-vscode": { - "resolved": "packages/vscode-ide-companion", - "link": true - }, "node_modules/@vscode/vsce": { "version": "3.9.2", "resolved": "https://registry.npmmirror.com/@vscode/vsce/-/vsce-3.9.2.tgz", @@ -2922,6 +2918,10 @@ "dev": true, "license": "MIT" }, + "node_modules/deepcode-vscode": { + "resolved": "packages/vscode-ide-companion", + "link": true + }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", @@ -7508,7 +7508,7 @@ } }, "packages/vscode-ide-companion": { - "name": "@vegamo/deepcode-vscode", + "name": "deepcode-vscode", "version": "0.1.22", "license": "MIT", "dependencies": { From 6014c9506891d0485ad717ce10e0e8245c23867c Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 16 Jun 2026 14:34:49 +0800 Subject: [PATCH 166/212] fix: remove .vscodeignore to resolve conflict with files property --- packages/vscode-ide-companion/.vscodeignore | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 packages/vscode-ide-companion/.vscodeignore diff --git a/packages/vscode-ide-companion/.vscodeignore b/packages/vscode-ide-companion/.vscodeignore deleted file mode 100644 index 68a0eac7..00000000 --- a/packages/vscode-ide-companion/.vscodeignore +++ /dev/null @@ -1,8 +0,0 @@ -.vscode/** -.vscodeignore -node_modules/** -src/** -tsconfig*.json -**/*.ts -!dist/** -!LICENSE From b8ab68bbf43f9104753868ad0b7f238a4b76a377 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 16 Jun 2026 14:55:31 +0800 Subject: [PATCH 167/212] fix: use double quotes in scripts for Windows compatibility --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a0e4e793..d017874d 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "typecheck": "npm run typecheck --workspaces --if-present", "generate": "node scripts/generate-git-commit-info.js", "bundle": "npm run generate && node scripts/esbuild.config.js && node scripts/copy-bundle-assets.js", - "lint": "eslint 'packages/*/src/**/*.{ts,tsx}' 'scripts/*.js'", - "lint:fix": "eslint 'packages/*/src/**/*.{ts,tsx}' 'scripts/*.js' --fix", - "format": "prettier --write 'packages/*/src/**/*.{ts,tsx}' 'scripts/*.js'", - "format:check": "prettier --check 'packages/*/src/**/*.{ts,tsx}' 'scripts/*.js'", + "lint": "eslint \"packages/*/src/**/*.{ts,tsx}\" \"scripts/*.js\"", + "lint:fix": "eslint \"packages/*/src/**/*.{ts,tsx}\" \"scripts/*.js\" --fix", + "format": "prettier --write \"packages/*/src/**/*.{ts,tsx}\" \"scripts/*.js\"", + "format:check": "prettier --check \"packages/*/src/**/*.{ts,tsx}\" \"scripts/*.js\"", "check": "npm run typecheck && npm run lint && npm run format:check", "clean": "node scripts/clean.js", "build": "node scripts/build.js", From 1779c6e01c43e6fc4e3c71b8432b0ec2e9600c90 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 16 Jun 2026 15:49:06 +0800 Subject: [PATCH 168/212] =?UTF-8?q?chore(core):=20=E6=9B=B4=E6=96=B0=20ski?= =?UTF-8?q?ll-digester=20=E8=AF=B4=E6=98=8E=E5=8F=8A=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 skill-digester 的 description 以覆盖技能安装和消化两大功能 - 细化消化技能的工作流步骤,增加更多操作细节和示例命令 - 新增安装 Agent Skill 工作流,规范安装路径及用户交互方式 - 规范使用 AskUserQuestion 提问,涵盖语言选择、修改确认与安装范围选择 - 修正示例 JSON 格式,统一提问选项结构 - 强化技能识别与路径匹配规则,避免误修改及覆盖风险 - 升级包版本号至 0.1.31,并统一项目包名为 @vegamo/deepcode --- package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- .../skills/bundled/skill-digester/SKILL.md | 207 ++++++++++-------- 4 files changed, 122 insertions(+), 91 deletions(-) diff --git a/package.json b/package.json index d017874d..e2a722c0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "deepcode", + "name": "@vegamo/deepcode", "description": "Deep Code — CLI, core library, and VSCode companion", "license": "MIT", "packageManager": "npm@10.9.4", diff --git a/packages/cli/package.json b/packages/cli/package.json index 21e7ae8e..654038ee 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.30", + "version": "0.1.31", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", diff --git a/packages/core/package.json b/packages/core/package.json index eb73e22a..1f924389 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-core", - "version": "0.1.30", + "version": "0.1.31", "description": "Deep Code core library — LLM session management, tool execution, and shared utilities", "license": "MIT", "type": "module", diff --git a/packages/core/templates/skills/bundled/skill-digester/SKILL.md b/packages/core/templates/skills/bundled/skill-digester/SKILL.md index 3bef806b..9a0f2675 100644 --- a/packages/core/templates/skills/bundled/skill-digester/SKILL.md +++ b/packages/core/templates/skills/bundled/skill-digester/SKILL.md @@ -1,87 +1,136 @@ --- name: skill-digester -description: Reviews and improves another DeepCode skill's SKILL.md description field against the Agent Skills description-field rules. Use when the user asks to "digest" a skill, including requests like "digest the pdf skill" or "消化 pdf 技能". +description: Reviews and improves another DeepCode skill's SKILL.md description field, and guides Agent Skill installation into user or project .agents/skills roots. Use when the user asks to digest a skill, install an Agent Skill, install a skill to user/project scope, or says "消化技能" or "安装 agent skill". --- # Skill Digester -Use this skill to review and optionally rewrite the `description` field of another DeepCode skill. +Use this skill for two related tasks: + +- Review and optionally rewrite the `description` field of another DeepCode skill. +- Guide installation of an Agent Skill into an interoperable `.agents/skills` root. ## Interaction Rule -Whenever user input is needed, call the `AskUserQuestion` tool. Do not ask follow-up questions as plain assistant text. This includes missing skill names, language preference, duplicate matches, malformed frontmatter decisions, and whether to apply a recommended rewrite. +Whenever user input is needed, call the `AskUserQuestion` tool. Do not ask follow-up questions as plain assistant text. This includes missing skill names or paths, install scope, language preference, duplicate matches, malformed frontmatter decisions, and whether to apply a recommended rewrite. ## Workflow +First classify the request: + +- If the user asks to install, add, copy, or place an Agent Skill, use the [Install Agent Skill Workflow](#install-agent-skill-workflow). +- Otherwise, use the [Digest Description Workflow](#digest-description-workflow). + +## Digest Description Workflow + 1. Identify the target skill from the user's request. - - If the user did not provide a skill name, use `AskUserQuestion` to ask for one. - - Locate the skill by running the bundled Node script from this skill directory: - - ```bash - node ~/.deepcode/skills/skill-digester/scripts/find-skill.js "" "" - ``` - - If this skill is loaded from a project-level or different user-level path, use the `scripts/find-skill.js` file next to this `SKILL.md` instead. - - - The script searches the same roots Deep Code CLI scans, in priority order: - 1. Project native skills: `./.deepcode/skills//SKILL.md` - 2. Project interoperable skills: `./.agents/skills//SKILL.md` - 3. User native skills: `~/.deepcode/skills//SKILL.md` - 4. User interoperable skills: `~/.agents/skills//SKILL.md` - - Treat `./` as the current Deep Code project root only; do not scan parent directories unless the running project root is changed. - - The script resolves each candidate's skill name the way Deep Code does: use the trimmed frontmatter `name` when present, otherwise use the folder name with underscores converted to hyphens. - - Match the user's input against the resolved skill name first. If needed, also consider the folder name or an explicit path the user provided. - - Treat the matched skill's `path` as the source `SKILL.md` to review. - - Treat the matched skill's `digestTarget.path` as the only output `SKILL.md` path to create or edit. - - `digestTarget.path` always points to the same scope's native Deep Code root: - - Project sources from `./.deepcode/skills` or `./.agents/skills` digest to `./.deepcode/skills//SKILL.md`. - - User sources from `~/.deepcode/skills` or `~/.agents/skills` digest to `~/.deepcode/skills//SKILL.md`. - - If the script returns one active match, use its `path` for reading and `digestTarget.path` for writing. - - If the script returns active and shadowed matches, present each source path and digest target path, then use `AskUserQuestion` before using a shadowed source. - - If the script returns no match, state that the skill was not found in Deep Code's scanned skill roots and use `AskUserQuestion` to ask whether the user wants to try another name. + - If the user did not provide a skill name, use `AskUserQuestion` to ask for one. + - Locate the skill by running the bundled Node script from this skill directory: + + ```bash + node ~/.deepcode/skills/skill-digester/scripts/find-skill.js "" "" + ``` + + If this skill is loaded from a project-level or different user-level path, use the `scripts/find-skill.js` file next to this `SKILL.md` instead. + - The script searches the same roots Deep Code CLI scans, in priority order: + 1. Project native skills: `./.deepcode/skills//SKILL.md` + 2. Project interoperable skills: `./.agents/skills//SKILL.md` + 3. User native skills: `~/.deepcode/skills//SKILL.md` + 4. User interoperable skills: `~/.agents/skills//SKILL.md` + - Treat `./` as the current Deep Code project root only; do not scan parent directories unless the running project root is changed. + - The script resolves each candidate's skill name the way Deep Code does: use the trimmed frontmatter `name` when present, otherwise use the folder name with underscores converted to hyphens. + - Match the user's input against the resolved skill name first. If needed, also consider the folder name or an explicit path the user provided. + - Treat the matched skill's `path` as the source `SKILL.md` to review. + - Treat the matched skill's `digestTarget.path` as the only output `SKILL.md` path to create or edit. + - `digestTarget.path` always points to the same scope's native Deep Code root: + - Project sources from `./.deepcode/skills` or `./.agents/skills` digest to `./.deepcode/skills//SKILL.md`. + - User sources from `~/.deepcode/skills` or `~/.agents/skills` digest to `~/.deepcode/skills//SKILL.md`. + - If the script returns one active match, use its `path` for reading and `digestTarget.path` for writing. + - If the script returns active and shadowed matches, present each source path and digest target path, then use `AskUserQuestion` before using a shadowed source. + - If the script returns no match, state that the skill was not found in Deep Code's scanned skill roots and use `AskUserQuestion` to ask whether the user wants to try another name. 2. Infer the user's preferred language before reviewing. - - Infer a likely language from the user's wording. For example, if the user says `消化pdf技能`, infer Chinese. - - Confirm the language with `AskUserQuestion` in the inferred language. For Chinese, ask: `请选择您偏好的语言。` - - Offer the inferred language first and include `English` as a fallback. The UI provides an `Other` option, so the user can type a different language. - - Use the confirmed preferred language for every later question, recommendation, and rewritten `description` field. + - Infer a likely language from the user's wording. For example, if the user says `消化pdf技能`, infer Chinese. + - Confirm the language with `AskUserQuestion` in the inferred language. For Chinese, ask: `请选择您偏好的语言。` + - Offer the inferred language first and include `English` as a fallback. The UI provides an `Other` option, so the user can type a different language. + - Use the confirmed preferred language for every later question, recommendation, and rewritten `description` field. 3. Read the source `SKILL.md`. - - Parse the YAML frontmatter and Markdown body from the matched source path. - - Preserve all frontmatter fields and body content except for the `description` field if the user approves a rewrite. - - If frontmatter is missing or malformed, explain the issue and use `AskUserQuestion` before making structural repairs. + - Parse the YAML frontmatter and Markdown body from the matched source path. + - Preserve all frontmatter fields and body content except for the `description` field if the user approves a rewrite. + - If frontmatter is missing or malformed, explain the issue and use `AskUserQuestion` before making structural repairs. 4. Review the current `description` field against the Agent Skills specification. - - Required constraints: - - It must be non-empty. - - It must be 1-1024 characters. - - It should describe what the skill does. - - It should describe when to use the skill. - - It should include specific keywords that help agents identify relevant tasks. - - Compare the description with the actual `SKILL.md` body. Flag mismatches, missing capabilities, overbroad activation language, vague wording, or important trigger keywords that are absent. - - Do not rewrite for style alone if the existing description is accurate, specific, and useful. + - Required constraints: + - It must be non-empty. + - It must be 1-1024 characters. + - It should describe what the skill does. + - It should describe when to use the skill. + - It should include specific keywords that help agents identify relevant tasks. + - Compare the description with the actual `SKILL.md` body. Flag mismatches, missing capabilities, overbroad activation language, vague wording, or important trigger keywords that are absent. + - Do not rewrite for style alone if the existing description is accurate, specific, and useful. 5. Present the review and recommendation. - - If the description is already good, say so and do not change the file unless the user asks. - - If improvements are useful, show: - - The current description. - - Concise review findings. - - A recommended replacement written in the preferred language. - - The source path being reviewed. - - The digest output path that would be created or edited. - - Use `AskUserQuestion` to ask the user to choose one of three actions in the preferred language: - - Apply the recommended change. - - Abandon the change. - - Continue discussing the wording. + - If the description is already good, say so and do not change the file unless the user asks. + - If improvements are useful, show: + - The current description. + - Concise review findings. + - A recommended replacement written in the preferred language. + - The source path being reviewed. + - The digest output path that would be created or edited. + - Use `AskUserQuestion` to ask the user to choose one of three actions in the preferred language: + - Apply the recommended change. + - Abandon the change. + - Continue discussing the wording. 6. Apply the change only after explicit approval. - - Write only to `digestTarget.path`; never write the digested result to `.agents/skills`. - - If `digestTarget.sameAsSource` is true, update only the `description` field in that existing native `SKILL.md`. - - If `digestTarget.sameAsSource` is false and `digestTarget.exists` is false, create the native target skill directory by copying the source skill directory first, then update only the target `SKILL.md` description. This preserves bundled scripts, references, and assets. - - If `digestTarget.sameAsSource` is false and `digestTarget.exists` is true, update only the `description` field in the existing native target `SKILL.md`; do not overwrite its body or bundled files unless the user explicitly asks. - - Keep the original `name` and any other frontmatter fields unchanged in the file being written. - - Preserve body content exactly unless the user separately asks to edit it. - - After editing, report the source path, updated digest output path, and final description. + - Write only to `digestTarget.path`; never write the digested result to `.agents/skills`. + - If `digestTarget.sameAsSource` is true, update only the `description` field in that existing native `SKILL.md`. + - If `digestTarget.sameAsSource` is false and `digestTarget.exists` is false, create the native target skill directory by copying the source skill directory first, then update only the target `SKILL.md` description. This preserves bundled scripts, references, and assets. + - If `digestTarget.sameAsSource` is false and `digestTarget.exists` is true, update only the `description` field in the existing native target `SKILL.md`; do not overwrite its body or bundled files unless the user explicitly asks. + - Keep the original `name` and any other frontmatter fields unchanged in the file being written. + - Preserve body content exactly unless the user separately asks to edit it. + - After editing, report the source path, updated digest output path, and final description. + +## Install Agent Skill Workflow + +Use this workflow when the user asks to install an Agent Skill. Installation always writes to `.agents/skills`, not `.deepcode/skills`. + +1. Identify the source skill directory. + - If the user provided an explicit file or directory path, resolve it: + - `~/...` relative to the user's home directory. + - `./...` relative to the current project root. + - Absolute paths as written. + - A `SKILL.md` path means its parent directory is the source skill directory. + - If the user provided a skill name instead of a path, locate it with `scripts/find-skill.js` using the same command and match rules as the digest workflow. + - If the user did not provide a skill name or path, use `AskUserQuestion` to ask for the source skill name or path. + - The source directory must contain `SKILL.md`. If it does not, report that the path is not an Agent Skill and ask for another source only if the user still wants to install. + +2. Determine the installed skill folder name. + - Parse the source `SKILL.md` frontmatter. + - Use the trimmed frontmatter `name` when present. + - Otherwise use the source folder name with underscores converted to hyphens. + - Use that resolved name as the target folder name. + +3. Ask exactly one installation scope question. + - Use `AskUserQuestion` to ask whether to install the skill at user level or project level. + - Offer only these scope choices: + - User-level install: `~/.agents/skills//` + - Project-level install: `./.agents/skills//` + - Do not ask any other installation preference before copying. + +4. Copy the complete skill directory. + - User-level destination: `~/.agents/skills//`. + - Project-level destination: `./.agents/skills//`. + - Copy the whole source skill directory, including `SKILL.md`, `references/`, `scripts/`, `templates/`, examples, assets, and other support files. + - Preserve file contents and relative paths exactly. + - Create the `.agents/skills` parent directory if needed. + - If the destination directory already exists, stop and report the conflict. Do not overwrite or merge files unless the user explicitly asks in a later message. + +5. Report the result. + - Report the source directory and installation destination. + - Mention that the agent client may need to reload or restart before the installed skill appears. + - Do not digest, rewrite, or normalize the installed skill unless the user separately asks for that. ## AskUserQuestion Patterns @@ -90,35 +139,15 @@ Use one question at a time unless two decisions are tightly coupled. Each questi Examples: ```json -{ - "questions": [ - { - "question": "请选择您偏好的语言。", - "options": [ - { "label": "中文", "description": "后续询问和推荐描述都使用中文。" }, - { "label": "English", "description": "Use English for follow-up questions and the recommended description." } - ] - } - ] -} +{"questions":[{"question":"请选择您偏好的语言。","options":[{"label":"中文","description":"后续询问和推荐描述都使用中文。"},{"label":"English","description":"Use English for follow-up questions and the recommended description."}]}]} +``` + +```json +{"questions":[{"question":"How should I proceed with this description recommendation?","options":[{"label":"Apply change","description":"Update only the description field in the native digest output SKILL.md."},{"label":"Abandon change","description":"Leave the file unchanged."},{"label":"Discuss wording","description":"Continue refining the proposed description before editing."}]}]} ``` ```json -{ - "questions": [ - { - "question": "How should I proceed with this description recommendation?", - "options": [ - { - "label": "Apply change", - "description": "Update only the description field in the native digest output SKILL.md." - }, - { "label": "Abandon change", "description": "Leave the file unchanged." }, - { "label": "Discuss wording", "description": "Continue refining the proposed description before editing." } - ] - } - ] -} +{"questions":[{"question":"Where should I install this Agent Skill?","options":[{"label":"User-level","description":"Install to ~/.agents/skills so it is available across projects."},{"label":"Project-level","description":"Install to ./.agents/skills so it is available in this project."}]}]} ``` ## Review Heuristics @@ -135,5 +164,7 @@ Avoid descriptions that are only generic labels, marketing copy, or internal imp - Never modify a different skill with a similar name without asking. - Never save the digested output under `.agents/skills`; `.agents/skills` is only a source root for digestion. +- Never save installed Agent Skills under `.deepcode/skills`; installation writes only to `.agents/skills`. - Never move a skill between project and user level during digestion. -- Never change the target skill's language preference after confirmation unless the user asks. +- Never overwrite or merge an existing installed skill directory unless the user explicitly asks after seeing the conflict. +- Never change the target skill's language preference after confirmation unless the user asks. \ No newline at end of file From 1d7abe7c363461d884c8eb7eb142738e21a621ab Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 17 Jun 2026 17:26:01 +0800 Subject: [PATCH 169/212] =?UTF-8?q?chore(vscode-ide-companion):=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20package.json=20=E5=88=86=E7=B1=BB=E5=92=8C?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将扩展类别从 "Other" 修改为 "AI" - 添加关键词列表,包括 deep-code、deep code、deep、code、cli、ide integration、ide companion - 设置扩展图标路径为 resources/deepcoding_icon.png --- packages/vscode-ide-companion/package.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 78ff140e..5ad51fb7 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -15,7 +15,16 @@ "vscode": "^1.85.0" }, "categories": [ - "Other" + "AI" + ], + "keywords": [ + "deep-code", + "deep code", + "deep", + "code", + "cli", + "ide integration", + "ide companion" ], "icon": "resources/deepcoding_icon.png", "activationEvents": [], From 8979f072560bbf3cd43dbc252d1c1e890c47d0dd Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 17 Jun 2026 17:28:40 +0800 Subject: [PATCH 170/212] =?UTF-8?q?chore(git):=20=E6=9B=B4=E6=96=B0.gitign?= =?UTF-8?q?ore=E4=BB=A5=E5=BF=BD=E7=95=A5=E7=94=9F=E6=88=90=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了对 packages/cli/src/generated/ 目录的忽略规则 - 添加了对 packages/core/src/generated/ 目录的忽略规则 - 添加了对 packages/vscode-ide-companion/ 下所有 .vsix 文件的忽略规则 --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index cd80bbf9..2052d018 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,9 @@ src/generated/ *.log *.vsix .deepcode/settings.json + + +# Generated files +packages/cli/src/generated/ +packages/core/src/generated/ +packages/vscode-ide-companion/*.vsix \ No newline at end of file From 1081e1bfb81c0558cfe2535861f65cdd53ff7bfd Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 17 Jun 2026 17:29:52 +0800 Subject: [PATCH 171/212] =?UTF-8?q?chore(git):=20=E6=9B=B4=E6=96=B0.gitign?= =?UTF-8?q?ore=E4=BB=A5=E5=BF=BD=E7=95=A5=E7=94=9F=E6=88=90=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了对 packages/cli/src/generated/ 目录的忽略规则 - 添加了对 packages/core/src/generated/ 目录的忽略规则 - 添加了对 packages/vscode-ide-companion/ 下所有 .vsix 文件的忽略规则 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2052d018..cceec9f7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ src/generated/ *.vsix .deepcode/settings.json +# TypeScript build info files +*.tsbuildinfo # Generated files packages/cli/src/generated/ From c0a2c903f379592d77689e9d34d4e1ae231a5662 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 17 Jun 2026 17:30:39 +0800 Subject: [PATCH 172/212] =?UTF-8?q?refactor(generated):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=E7=9A=84git?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=A1=E6=81=AF=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除packages/cli中的git-commit.ts自动生成文件 - 删除packages/core中的git-commit.ts自动生成文件 - 清理无用的自动生成代码以简化项目结构 --- packages/cli/src/generated/git-commit.ts | 8 -------- packages/core/src/generated/git-commit.ts | 8 -------- packages/core/tsconfig.tsbuildinfo | 1 - 3 files changed, 17 deletions(-) delete mode 100644 packages/cli/src/generated/git-commit.ts delete mode 100644 packages/core/src/generated/git-commit.ts delete mode 100644 packages/core/tsconfig.tsbuildinfo diff --git a/packages/cli/src/generated/git-commit.ts b/packages/cli/src/generated/git-commit.ts deleted file mode 100644 index e32594c8..00000000 --- a/packages/cli/src/generated/git-commit.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license - * Copyright 2026 @vegamo deepcode - */ - -// Auto-generated by scripts/generate-git-commit-info.js. Do not edit. -export const GIT_COMMIT_INFO = "cc7b0c3"; -export const CLI_VERSION = "0.1.30"; diff --git a/packages/core/src/generated/git-commit.ts b/packages/core/src/generated/git-commit.ts deleted file mode 100644 index e32594c8..00000000 --- a/packages/core/src/generated/git-commit.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license - * Copyright 2026 @vegamo deepcode - */ - -// Auto-generated by scripts/generate-git-commit-info.js. Do not edit. -export const GIT_COMMIT_INFO = "cc7b0c3"; -export const CLI_VERSION = "0.1.30"; diff --git a/packages/core/tsconfig.tsbuildinfo b/packages/core/tsconfig.tsbuildinfo deleted file mode 100644 index 5bef9a8e..00000000 --- a/packages/core/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"fileNames":["../../node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/typescript/lib/lib.es2021.d.ts","../../node_modules/typescript/lib/lib.es2022.d.ts","../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../node_modules/typescript/lib/lib.es2025.float16.d.ts","../../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","./src/common/model-capabilities.ts","./src/settings.ts","../../node_modules/gray-matter/gray-matter.d.ts","../../node_modules/@types/ejs/index.d.ts","../../node_modules/openai/internal/builtin-types.d.mts","../../node_modules/openai/internal/types.d.mts","../../node_modules/openai/internal/headers.d.mts","../../node_modules/openai/internal/shim-types.d.mts","../../node_modules/openai/core/streaming.d.mts","../../node_modules/openai/internal/request-options.d.mts","../../node_modules/openai/internal/utils/log.d.mts","../../node_modules/openai/resources/shared.d.mts","../../node_modules/openai/core/error.d.mts","../../node_modules/openai/pagination.d.mts","../../node_modules/openai/internal/parse.d.mts","../../node_modules/openai/core/api-promise.d.mts","../../node_modules/openai/core/pagination.d.mts","../../node_modules/openai/auth/types.d.mts","../../node_modules/openai/internal/uploads.d.mts","../../node_modules/openai/internal/to-file.d.mts","../../node_modules/openai/core/uploads.d.mts","../../node_modules/openai/resources/chat/chat.d.mts","../../node_modules/openai/resources/chat/index.d.mts","../../node_modules/openai/resources/admin/organization/admin-api-keys.d.mts","../../node_modules/openai/resources/admin/organization/audit-logs.d.mts","../../node_modules/openai/resources/admin/organization/certificates.d.mts","../../node_modules/openai/resources/admin/organization/data-retention.d.mts","../../node_modules/openai/resources/admin/organization/invites.d.mts","../../node_modules/openai/resources/admin/organization/roles.d.mts","../../node_modules/openai/resources/admin/organization/spend-alerts.d.mts","../../node_modules/openai/resources/admin/organization/usage.d.mts","../../node_modules/openai/resources/admin/organization/groups/roles.d.mts","../../node_modules/openai/resources/admin/organization/groups/users.d.mts","../../node_modules/openai/resources/admin/organization/groups/groups.d.mts","../../node_modules/openai/resources/admin/organization/projects/api-keys.d.mts","../../node_modules/openai/resources/admin/organization/projects/certificates.d.mts","../../node_modules/openai/resources/admin/organization/projects/data-retention.d.mts","../../node_modules/openai/resources/admin/organization/projects/hosted-tool-permissions.d.mts","../../node_modules/openai/resources/admin/organization/projects/model-permissions.d.mts","../../node_modules/openai/resources/admin/organization/projects/rate-limits.d.mts","../../node_modules/openai/resources/admin/organization/projects/roles.d.mts","../../node_modules/openai/resources/admin/organization/projects/service-accounts.d.mts","../../node_modules/openai/resources/admin/organization/projects/spend-alerts.d.mts","../../node_modules/openai/resources/admin/organization/projects/groups/roles.d.mts","../../node_modules/openai/resources/admin/organization/projects/groups/groups.d.mts","../../node_modules/openai/resources/admin/organization/users/roles.d.mts","../../node_modules/openai/resources/admin/organization/users/users.d.mts","../../node_modules/openai/resources/admin/organization/projects/users/roles.d.mts","../../node_modules/openai/resources/admin/organization/projects/users/users.d.mts","../../node_modules/openai/resources/admin/organization/projects/projects.d.mts","../../node_modules/openai/resources/admin/organization/organization.d.mts","../../node_modules/openai/resources/admin/admin.d.mts","../../node_modules/openai/resources/audio/speech.d.mts","../../node_modules/openai/resources/audio/transcriptions.d.mts","../../node_modules/openai/resources/audio/translations.d.mts","../../node_modules/openai/resources/audio/audio.d.mts","../../node_modules/openai/resources/batches.d.mts","../../node_modules/openai/resources/beta/threads/messages.d.mts","../../node_modules/openai/resources/beta/threads/runs/steps.d.mts","../../node_modules/openai/error.d.mts","../../node_modules/openai/lib/eventstream.d.mts","../../node_modules/openai/lib/assistantstream.d.mts","../../node_modules/openai/resources/beta/threads/runs/runs.d.mts","../../node_modules/openai/resources/beta/threads/threads.d.mts","../../node_modules/openai/resources/beta/assistants.d.mts","../../node_modules/openai/resources/beta/realtime/sessions.d.mts","../../node_modules/openai/resources/beta/realtime/transcription-sessions.d.mts","../../node_modules/openai/resources/beta/realtime/realtime.d.mts","../../node_modules/openai/resources/beta/chatkit/threads.d.mts","../../node_modules/openai/resources/beta/chatkit/sessions.d.mts","../../node_modules/openai/resources/beta/chatkit/chatkit.d.mts","../../node_modules/openai/resources/beta/beta.d.mts","../../node_modules/openai/resources/completions.d.mts","../../node_modules/openai/lib/parser.d.mts","../../node_modules/openai/lib/responsesparser.d.mts","../../node_modules/openai/azure.d.mts","../../node_modules/openai/bedrock.d.mts","../../node_modules/openai/index.d.mts","../../node_modules/openai/lib/responses/eventtypes.d.mts","../../node_modules/openai/lib/responses/responsestream.d.mts","../../node_modules/openai/resources/responses/input-items.d.mts","../../node_modules/openai/resources/responses/input-tokens.d.mts","../../node_modules/openai/resources/responses/responses.d.mts","../../node_modules/openai/resources/containers/files/content.d.mts","../../node_modules/openai/resources/containers/files/files.d.mts","../../node_modules/openai/resources/containers/containers.d.mts","../../node_modules/openai/resources/conversations/items.d.mts","../../node_modules/openai/resources/conversations/conversations.d.mts","../../node_modules/openai/resources/embeddings.d.mts","../../node_modules/openai/resources/graders/grader-models.d.mts","../../node_modules/openai/resources/evals/runs/output-items.d.mts","../../node_modules/openai/resources/evals/runs/runs.d.mts","../../node_modules/openai/resources/evals/evals.d.mts","../../node_modules/openai/resources/files.d.mts","../../node_modules/openai/resources/fine-tuning/methods.d.mts","../../node_modules/openai/resources/fine-tuning/alpha/graders.d.mts","../../node_modules/openai/resources/fine-tuning/alpha/alpha.d.mts","../../node_modules/openai/resources/fine-tuning/checkpoints/permissions.d.mts","../../node_modules/openai/resources/fine-tuning/checkpoints/checkpoints.d.mts","../../node_modules/openai/resources/fine-tuning/jobs/checkpoints.d.mts","../../node_modules/openai/resources/fine-tuning/jobs/jobs.d.mts","../../node_modules/openai/resources/fine-tuning/fine-tuning.d.mts","../../node_modules/openai/resources/graders/graders.d.mts","../../node_modules/openai/resources/images.d.mts","../../node_modules/openai/resources/models.d.mts","../../node_modules/openai/resources/moderations.d.mts","../../node_modules/openai/resources/realtime/calls.d.mts","../../node_modules/openai/resources/realtime/client-secrets.d.mts","../../node_modules/openai/resources/realtime/realtime.d.mts","../../node_modules/openai/resources/skills/content.d.mts","../../node_modules/openai/resources/skills/versions/content.d.mts","../../node_modules/openai/resources/skills/versions/versions.d.mts","../../node_modules/openai/resources/skills/skills.d.mts","../../node_modules/openai/resources/uploads/parts.d.mts","../../node_modules/openai/resources/uploads/uploads.d.mts","../../node_modules/openai/uploads.d.mts","../../node_modules/openai/resources/vector-stores/files.d.mts","../../node_modules/openai/resources/vector-stores/file-batches.d.mts","../../node_modules/openai/resources/vector-stores/vector-stores.d.mts","../../node_modules/openai/resources/videos.d.mts","../../node_modules/openai/resources/webhooks/webhooks.d.mts","../../node_modules/openai/resources/webhooks/index.d.mts","../../node_modules/openai/resources/webhooks.d.mts","../../node_modules/openai/resources/index.d.mts","../../node_modules/openai/client.d.mts","../../node_modules/openai/core/resource.d.mts","../../node_modules/openai/resources/chat/completions/messages.d.mts","../../node_modules/openai/lib/abstractchatcompletionrunner.d.mts","../../node_modules/openai/lib/chatcompletionstream.d.mts","../../node_modules/openai/lib/chatcompletionstreamingrunner.d.mts","../../node_modules/openai/lib/jsonschema.d.mts","../../node_modules/openai/lib/runnablefunction.d.mts","../../node_modules/openai/lib/chatcompletionrunner.d.mts","../../node_modules/openai/resources/chat/completions/completions.d.mts","../../node_modules/openai/resources/chat/completions/index.d.mts","../../node_modules/openai/resources/chat/completions.d.mts","./src/common/notify.ts","./src/common/openai-thinking.ts","./src/common/shell-utils.ts","./src/common/state.ts","./src/common/file-utils.ts","./src/prompt.ts","./src/tools/ask-user-question-handler.ts","./src/common/bash-timeout.ts","./src/common/process-tree.ts","./src/tools/bash-handler.ts","../../node_modules/zod/v4/core/json-schema.d.cts","../../node_modules/zod/v4/core/standard-schema.d.cts","../../node_modules/zod/v4/core/registries.d.cts","../../node_modules/zod/v4/core/to-json-schema.d.cts","../../node_modules/zod/v4/core/util.d.cts","../../node_modules/zod/v4/core/versions.d.cts","../../node_modules/zod/v4/core/schemas.d.cts","../../node_modules/zod/v4/core/checks.d.cts","../../node_modules/zod/v4/core/errors.d.cts","../../node_modules/zod/v4/core/core.d.cts","../../node_modules/zod/v4/core/parse.d.cts","../../node_modules/zod/v4/core/regexes.d.cts","../../node_modules/zod/v4/locales/ar.d.cts","../../node_modules/zod/v4/locales/az.d.cts","../../node_modules/zod/v4/locales/be.d.cts","../../node_modules/zod/v4/locales/bg.d.cts","../../node_modules/zod/v4/locales/ca.d.cts","../../node_modules/zod/v4/locales/cs.d.cts","../../node_modules/zod/v4/locales/da.d.cts","../../node_modules/zod/v4/locales/de.d.cts","../../node_modules/zod/v4/locales/el.d.cts","../../node_modules/zod/v4/locales/en.d.cts","../../node_modules/zod/v4/locales/eo.d.cts","../../node_modules/zod/v4/locales/es.d.cts","../../node_modules/zod/v4/locales/fa.d.cts","../../node_modules/zod/v4/locales/fi.d.cts","../../node_modules/zod/v4/locales/fr.d.cts","../../node_modules/zod/v4/locales/fr-ca.d.cts","../../node_modules/zod/v4/locales/he.d.cts","../../node_modules/zod/v4/locales/hr.d.cts","../../node_modules/zod/v4/locales/hu.d.cts","../../node_modules/zod/v4/locales/hy.d.cts","../../node_modules/zod/v4/locales/id.d.cts","../../node_modules/zod/v4/locales/is.d.cts","../../node_modules/zod/v4/locales/it.d.cts","../../node_modules/zod/v4/locales/ja.d.cts","../../node_modules/zod/v4/locales/ka.d.cts","../../node_modules/zod/v4/locales/kh.d.cts","../../node_modules/zod/v4/locales/km.d.cts","../../node_modules/zod/v4/locales/ko.d.cts","../../node_modules/zod/v4/locales/lt.d.cts","../../node_modules/zod/v4/locales/mk.d.cts","../../node_modules/zod/v4/locales/ms.d.cts","../../node_modules/zod/v4/locales/nl.d.cts","../../node_modules/zod/v4/locales/no.d.cts","../../node_modules/zod/v4/locales/ota.d.cts","../../node_modules/zod/v4/locales/ps.d.cts","../../node_modules/zod/v4/locales/pl.d.cts","../../node_modules/zod/v4/locales/pt.d.cts","../../node_modules/zod/v4/locales/ro.d.cts","../../node_modules/zod/v4/locales/ru.d.cts","../../node_modules/zod/v4/locales/sl.d.cts","../../node_modules/zod/v4/locales/sv.d.cts","../../node_modules/zod/v4/locales/ta.d.cts","../../node_modules/zod/v4/locales/th.d.cts","../../node_modules/zod/v4/locales/tr.d.cts","../../node_modules/zod/v4/locales/ua.d.cts","../../node_modules/zod/v4/locales/uk.d.cts","../../node_modules/zod/v4/locales/ur.d.cts","../../node_modules/zod/v4/locales/uz.d.cts","../../node_modules/zod/v4/locales/vi.d.cts","../../node_modules/zod/v4/locales/zh-cn.d.cts","../../node_modules/zod/v4/locales/zh-tw.d.cts","../../node_modules/zod/v4/locales/yo.d.cts","../../node_modules/zod/v4/locales/index.d.cts","../../node_modules/zod/v4/core/doc.d.cts","../../node_modules/zod/v4/core/api.d.cts","../../node_modules/zod/v4/core/json-schema-processors.d.cts","../../node_modules/zod/v4/core/json-schema-generator.d.cts","../../node_modules/zod/v4/core/index.d.cts","../../node_modules/zod/v4/classic/errors.d.cts","../../node_modules/zod/v4/classic/parse.d.cts","../../node_modules/zod/v4/classic/schemas.d.cts","../../node_modules/zod/v4/classic/checks.d.cts","../../node_modules/zod/v4/classic/compat.d.cts","../../node_modules/zod/v4/classic/from-json-schema.d.cts","../../node_modules/zod/v4/classic/iso.d.cts","../../node_modules/zod/v4/classic/coerce.d.cts","../../node_modules/zod/v4/classic/external.d.cts","../../node_modules/zod/index.d.cts","./src/common/tool-types.ts","./src/common/validate.ts","./src/tools/edit-handler.ts","./node_modules/ignore/index.d.ts","./src/tools/read-handler.ts","./src/tools/update-plan-handler.ts","./src/tools/web-search-handler.ts","./src/tools/write-handler.ts","./src/mcp/mcp-client.ts","./src/mcp/mcp-manager.ts","./src/tools/executor.ts","./src/common/error-logger.ts","./src/common/debug-logger.ts","./src/common/file-history.ts","./src/common/permissions.ts","./src/common/telemetry.ts","./src/common/openai-message-converter.ts","./src/session.ts","../../node_modules/undici/types/utility.d.ts","../../node_modules/undici/types/header.d.ts","../../node_modules/undici/types/readable.d.ts","../../node_modules/@types/node/compatibility/iterators.d.ts","../../node_modules/@types/node/globals.typedarray.d.ts","../../node_modules/@types/node/buffer.buffer.d.ts","../../node_modules/@types/node/globals.d.ts","../../node_modules/@types/node/web-globals/abortcontroller.d.ts","../../node_modules/@types/node/web-globals/blob.d.ts","../../node_modules/@types/node/web-globals/console.d.ts","../../node_modules/@types/node/web-globals/crypto.d.ts","../../node_modules/@types/node/web-globals/domexception.d.ts","../../node_modules/@types/node/web-globals/encoding.d.ts","../../node_modules/@types/node/web-globals/events.d.ts","../../node_modules/undici-types/utility.d.ts","../../node_modules/undici-types/header.d.ts","../../node_modules/undici-types/readable.d.ts","../../node_modules/undici-types/fetch.d.ts","../../node_modules/undici-types/formdata.d.ts","../../node_modules/undici-types/connector.d.ts","../../node_modules/undici-types/client-stats.d.ts","../../node_modules/undici-types/client.d.ts","../../node_modules/undici-types/errors.d.ts","../../node_modules/undici-types/dispatcher.d.ts","../../node_modules/undici-types/global-dispatcher.d.ts","../../node_modules/undici-types/global-origin.d.ts","../../node_modules/undici-types/pool-stats.d.ts","../../node_modules/undici-types/pool.d.ts","../../node_modules/undici-types/handlers.d.ts","../../node_modules/undici-types/balanced-pool.d.ts","../../node_modules/undici-types/round-robin-pool.d.ts","../../node_modules/undici-types/h2c-client.d.ts","../../node_modules/undici-types/agent.d.ts","../../node_modules/undici-types/mock-interceptor.d.ts","../../node_modules/undici-types/mock-call-history.d.ts","../../node_modules/undici-types/mock-agent.d.ts","../../node_modules/undici-types/mock-client.d.ts","../../node_modules/undici-types/mock-pool.d.ts","../../node_modules/undici-types/snapshot-agent.d.ts","../../node_modules/undici-types/mock-errors.d.ts","../../node_modules/undici-types/proxy-agent.d.ts","../../node_modules/undici-types/socks5-proxy-agent.d.ts","../../node_modules/undici-types/env-http-proxy-agent.d.ts","../../node_modules/undici-types/retry-handler.d.ts","../../node_modules/undici-types/retry-agent.d.ts","../../node_modules/undici-types/api.d.ts","../../node_modules/undici-types/cache-interceptor.d.ts","../../node_modules/undici-types/interceptors.d.ts","../../node_modules/undici-types/util.d.ts","../../node_modules/undici-types/cookies.d.ts","../../node_modules/undici-types/patch.d.ts","../../node_modules/undici-types/websocket.d.ts","../../node_modules/undici-types/eventsource.d.ts","../../node_modules/undici-types/diagnostics-channel.d.ts","../../node_modules/undici-types/content-type.d.ts","../../node_modules/undici-types/cache.d.ts","../../node_modules/undici-types/index.d.ts","../../node_modules/@types/node/web-globals/fetch.d.ts","../../node_modules/@types/node/web-globals/importmeta.d.ts","../../node_modules/@types/node/web-globals/messaging.d.ts","../../node_modules/@types/node/web-globals/navigator.d.ts","../../node_modules/@types/node/web-globals/performance.d.ts","../../node_modules/@types/node/web-globals/storage.d.ts","../../node_modules/@types/node/web-globals/streams.d.ts","../../node_modules/@types/node/web-globals/timers.d.ts","../../node_modules/@types/node/web-globals/url.d.ts","../../node_modules/@types/node/assert.d.ts","../../node_modules/@types/node/assert/strict.d.ts","../../node_modules/@types/node/async_hooks.d.ts","../../node_modules/@types/node/buffer.d.ts","../../node_modules/@types/node/child_process.d.ts","../../node_modules/@types/node/cluster.d.ts","../../node_modules/@types/node/console.d.ts","../../node_modules/@types/node/constants.d.ts","../../node_modules/@types/node/crypto.d.ts","../../node_modules/@types/node/dgram.d.ts","../../node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/@types/node/dns.d.ts","../../node_modules/@types/node/dns/promises.d.ts","../../node_modules/@types/node/domain.d.ts","../../node_modules/@types/node/events.d.ts","../../node_modules/@types/node/fs.d.ts","../../node_modules/@types/node/fs/promises.d.ts","../../node_modules/@types/node/http.d.ts","../../node_modules/@types/node/http2.d.ts","../../node_modules/@types/node/https.d.ts","../../node_modules/@types/node/inspector.d.ts","../../node_modules/@types/node/inspector.generated.d.ts","../../node_modules/@types/node/inspector/promises.d.ts","../../node_modules/@types/node/module.d.ts","../../node_modules/@types/node/net.d.ts","../../node_modules/buffer/index.d.ts","../../node_modules/@types/node/os.d.ts","../../node_modules/@types/node/path.d.ts","../../node_modules/@types/node/path/posix.d.ts","../../node_modules/@types/node/path/win32.d.ts","../../node_modules/@types/node/perf_hooks.d.ts","../../node_modules/@types/node/process.d.ts","../../node_modules/@types/node/punycode.d.ts","../../node_modules/@types/node/querystring.d.ts","../../node_modules/@types/node/quic.d.ts","../../node_modules/@types/node/readline.d.ts","../../node_modules/@types/node/readline/promises.d.ts","../../node_modules/@types/node/repl.d.ts","../../node_modules/@types/node/sea.d.ts","../../node_modules/@types/node/sqlite.d.ts","../../node_modules/@types/node/stream.d.ts","../../node_modules/@types/node/stream/consumers.d.ts","../../node_modules/@types/node/stream/iter.d.ts","../../node_modules/@types/node/stream/promises.d.ts","../../node_modules/@types/node/stream/web.d.ts","../../node_modules/@types/node/string_decoder.d.ts","../../node_modules/@types/node/test.d.ts","../../node_modules/@types/node/test/reporters.d.ts","../../node_modules/@types/node/timers.d.ts","../../node_modules/@types/node/timers/promises.d.ts","../../node_modules/@types/node/tls.d.ts","../../node_modules/@types/node/trace_events.d.ts","../../node_modules/@types/node/tty.d.ts","../../node_modules/@types/node/url.d.ts","../../node_modules/@types/node/util.d.ts","../../node_modules/@types/node/util/types.d.ts","../../node_modules/@types/node/v8.d.ts","../../node_modules/@types/node/vm.d.ts","../../node_modules/@types/node/wasi.d.ts","../../node_modules/@types/node/worker_threads.d.ts","../../node_modules/@types/node/zlib.d.ts","../../node_modules/@types/node/zlib/iter.d.ts","../../node_modules/@types/node/index.d.ts","../../node_modules/undici/types/fetch.d.ts","../../node_modules/undici/types/formdata.d.ts","../../node_modules/undici/types/connector.d.ts","../../node_modules/undici/types/client-stats.d.ts","../../node_modules/undici/types/client.d.ts","../../node_modules/undici/types/errors.d.ts","../../node_modules/undici/types/dispatcher.d.ts","../../node_modules/undici/types/global-dispatcher.d.ts","../../node_modules/undici/types/global-origin.d.ts","../../node_modules/undici/types/pool-stats.d.ts","../../node_modules/undici/types/pool.d.ts","../../node_modules/undici/types/handlers.d.ts","../../node_modules/undici/types/balanced-pool.d.ts","../../node_modules/undici/types/round-robin-pool.d.ts","../../node_modules/undici/types/h2c-client.d.ts","../../node_modules/undici/types/agent.d.ts","../../node_modules/undici/types/mock-interceptor.d.ts","../../node_modules/undici/types/mock-call-history.d.ts","../../node_modules/undici/types/mock-agent.d.ts","../../node_modules/undici/types/mock-client.d.ts","../../node_modules/undici/types/mock-pool.d.ts","../../node_modules/undici/types/snapshot-agent.d.ts","../../node_modules/undici/types/mock-errors.d.ts","../../node_modules/undici/types/proxy-agent.d.ts","../../node_modules/undici/types/socks5-proxy-agent.d.ts","../../node_modules/undici/types/env-http-proxy-agent.d.ts","../../node_modules/undici/types/retry-handler.d.ts","../../node_modules/undici/types/retry-agent.d.ts","../../node_modules/undici/types/api.d.ts","../../node_modules/undici/types/cache-interceptor.d.ts","../../node_modules/undici/types/interceptors.d.ts","../../node_modules/undici/types/util.d.ts","../../node_modules/undici/types/cookies.d.ts","../../node_modules/undici/types/patch.d.ts","../../node_modules/undici/types/websocket.d.ts","../../node_modules/undici/types/eventsource.d.ts","../../node_modules/undici/types/diagnostics-channel.d.ts","../../node_modules/undici/types/content-type.d.ts","../../node_modules/undici/types/cache.d.ts","../../node_modules/undici/types/index.d.ts","../../node_modules/undici/index.d.ts","./src/common/openai-client.ts","./src/index.ts"],"fileIdsList":[[309,373,381,385,388,390,391,392,405,434],[309,370,371,373,381,385,388,390,391,392,405,434],[309,372,373,381,385,388,390,391,392,405,434],[373,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,405,414,434],[309,373,374,379,381,384,385,388,390,391,392,394,405,410,423,434],[309,373,374,375,381,384,385,388,390,391,392,405,434],[309,373,376,381,385,388,390,391,392,405,424,434],[309,373,377,378,381,385,388,390,391,392,396,405,434],[309,373,378,381,385,388,390,391,392,405,410,420,434],[309,373,379,381,384,385,388,390,391,392,394,405,434],[309,372,373,380,381,385,388,390,391,392,405,434],[309,373,381,382,385,388,390,391,392,405,434],[309,373,381,383,384,385,388,390,391,392,405,434],[309,372,373,381,384,385,388,390,391,392,405,434],[309,373,381,384,385,386,388,390,391,392,405,410,423,434],[309,373,381,384,385,386,388,390,391,392,405,410,412,414,434],[309,360,373,381,384,385,387,388,390,391,392,394,405,410,423,434],[309,373,381,384,385,387,388,390,391,392,394,405,410,420,423,434],[309,373,381,385,387,388,389,390,391,392,405,410,420,423,434],[307,308,309,310,311,312,313,314,315,316,317,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,434],[309,373,381,384,385,388,390,391,392,405,434],[309,373,381,385,388,390,392,405,434],[309,373,381,385,388,390,391,392,393,405,423,434],[309,373,381,384,385,388,390,391,392,394,405,410,434],[309,373,381,385,388,390,391,392,396,405,434],[309,373,381,385,388,390,391,392,397,405,434],[309,373,381,384,385,388,390,391,392,400,405,434],[309,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,434],[309,373,381,385,388,390,391,392,402,405,434],[309,373,381,385,388,390,391,392,403,405,434],[309,373,378,381,385,388,390,391,392,394,405,414,434],[309,373,381,384,385,388,390,391,392,405,406,434],[309,373,381,385,388,390,391,392,405,407,424,427,434],[309,373,381,384,385,388,390,391,392,405,410,413,414,434],[309,373,381,385,388,390,391,392,405,411,414,434],[309,373,381,385,388,390,391,392,405,412,434],[309,373,381,385,388,390,391,392,405,414,424,434],[309,373,381,385,388,390,391,392,405,415,434],[309,370,373,381,385,388,390,391,392,405,410,417,423,434],[309,373,381,385,388,390,391,392,405,410,416,434],[309,373,381,384,385,388,390,391,392,405,418,419,434],[309,373,381,385,388,390,391,392,405,418,419,434],[309,373,378,381,385,388,390,391,392,394,405,410,420,434],[309,373,381,385,388,390,391,392,405,421,434],[309,373,381,385,388,390,391,392,394,405,422,434],[309,373,381,385,387,388,390,391,392,403,405,423,434],[309,373,381,385,388,390,391,392,405,424,425,434],[309,373,378,381,385,388,390,391,392,405,425,434],[309,373,381,385,388,390,391,392,405,410,426,434],[309,373,381,385,388,390,391,392,393,405,427,434],[309,373,381,385,388,390,391,392,405,428,434],[309,373,376,381,385,388,390,391,392,405,434],[309,373,378,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,405,424,434],[309,360,373,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,405,423,434],[309,373,381,385,388,390,391,392,405,429,434],[309,373,381,385,388,390,391,392,400,405,434],[309,373,381,385,388,390,391,392,405,419,434],[309,360,373,381,384,385,386,388,390,391,392,400,405,410,414,423,426,427,429,434],[309,373,381,385,388,390,391,392,405,410,430,434],[309,373,381,385,388,390,391,392,405,412,431,434],[64,66,69,184,309,373,381,385,388,390,391,392,405,434],[66,69,184,309,373,381,385,388,390,391,392,405,434],[64,65,66,69,70,72,75,76,77,80,81,111,115,116,131,132,142,145,147,148,152,153,161,162,163,164,165,168,172,174,178,179,180,183,193,309,373,381,385,388,390,391,392,405,434],[65,74,184,309,373,381,385,388,390,391,392,405,434],[71,309,373,381,385,388,390,391,392,405,434],[69,74,75,184,309,373,381,385,388,390,391,392,405,434],[184,309,373,381,385,388,390,391,392,405,434],[67,184,309,373,381,385,388,390,391,392,405,434],[78,79,309,373,381,385,388,390,391,392,405,434],[72,309,373,381,385,388,390,391,392,405,434],[72,75,76,80,135,136,184,309,373,381,385,388,390,391,392,405,434],[69,73,184,309,373,381,385,388,390,391,392,405,434],[64,65,66,68,309,373,381,385,388,390,391,392,405,434],[64,309,373,381,385,388,390,391,392,405,434],[64,69,184,309,373,381,385,388,390,391,392,405,434],[69,184,309,373,381,385,388,390,391,392,405,434],[69,120,132,137,189,191,192,195,309,373,381,385,388,390,391,392,405,434],[67,69,117,118,120,122,123,124,309,373,381,385,388,390,391,392,405,434],[133,137,187,191,195,309,373,381,385,388,390,391,392,405,434],[67,69,137,187,193,195,309,373,381,385,388,390,391,392,405,434],[67,133,137,187,188,191,195,309,373,381,385,388,390,391,392,405,434],[119,309,373,381,385,388,390,391,392,405,434],[71,142,195,309,373,381,385,388,390,391,392,405,434],[142,309,373,381,385,388,390,391,392,405,434],[69,120,134,137,138,142,309,373,381,385,388,390,391,392,405,434],[133,142,195,309,373,381,385,388,390,391,392,405,434],[189,190,192,309,373,381,385,388,390,391,392,405,434],[76,309,373,381,385,388,390,391,392,405,434],[110,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,185,309,373,381,385,388,390,391,392,405,434],[69,76,185,309,373,381,385,388,390,391,392,405,434],[69,75,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,91,92,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,88,185,309,373,381,385,388,390,391,392,405,434],[83,84,85,86,87,88,89,90,93,106,109,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,103,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,94,95,96,97,98,99,100,101,102,104,108,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,88,106,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,107,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,105,185,309,373,381,385,388,390,391,392,405,434],[112,113,114,185,309,373,381,385,388,390,391,392,405,434],[68,69,75,80,113,115,185,309,373,381,385,388,390,391,392,405,434],[69,75,80,113,115,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,116,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,117,118,121,122,123,185,309,373,381,385,388,390,391,392,405,434],[123,124,127,130,185,309,373,381,385,388,390,391,392,405,434],[128,129,185,309,373,381,385,388,390,391,392,405,434],[69,75,128,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,130,185,309,373,381,385,388,390,391,392,405,434],[71,125,126,127,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,124,185,309,373,381,385,388,390,391,392,405,434],[68,69,71,75,76,117,118,121,122,123,124,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,118,185,309,373,381,385,388,390,391,392,405,434],[68,69,71,75,117,121,122,123,124,185,309,373,381,385,388,390,391,392,405,434],[71,185,193,309,373,381,385,388,390,391,392,405,434],[194,309,373,381,385,388,390,391,392,405,434],[68,69,71,75,76,132,133,185,186,187,188,189,191,192,193,309,373,381,385,388,390,391,392,405,434],[186,193,309,373,381,385,388,390,391,392,405,434],[69,76,185,193,309,373,381,385,388,390,391,392,405,434],[81,194,309,373,381,385,388,390,391,392,405,434],[68,69,75,132,185,193,309,373,381,385,388,390,391,392,405,434],[69,75,76,142,144,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,80,143,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,142,146,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,142,147,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,142,149,151,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,151,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,142,149,150,185,193,309,373,381,385,388,390,391,392,405,434],[69,75,76,80,185,309,373,381,385,388,390,391,392,405,434],[155,185,309,373,381,385,388,390,391,392,405,434],[69,75,149,185,309,373,381,385,388,390,391,392,405,434],[157,185,309,373,381,385,388,390,391,392,405,434],[154,156,158,160,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,154,159,185,309,373,381,385,388,390,391,392,405,434],[149,185,309,373,381,385,388,390,391,392,405,434],[71,142,149,185,309,373,381,385,388,390,391,392,405,434],[68,69,75,80,163,185,309,373,381,385,388,390,391,392,405,434],[71,82,111,115,116,131,132,142,145,147,148,152,153,161,162,163,164,165,168,172,174,178,179,182,309,373,381,385,388,390,391,392,405,434],[69,75,142,168,185,309,373,381,385,388,390,391,392,405,434],[69,75,142,167,168,185,309,373,381,385,388,390,391,392,405,434],[71,142,166,167,168,185,309,373,381,385,388,390,391,392,405,434],[69,76,142,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,142,185,309,373,381,385,388,390,391,392,405,434],[68,69,71,75,76,134,139,140,141,142,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,80,169,171,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,80,170,185,309,373,381,385,388,390,391,392,405,434],[69,75,80,185,309,373,381,385,388,390,391,392,405,434],[69,75,153,173,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,175,176,178,185,309,373,381,385,388,390,391,392,405,434],[69,75,76,175,178,185,309,373,381,385,388,390,391,392,405,434],[69,71,75,76,176,177,185,309,373,381,385,388,390,391,392,405,434],[181,309,373,381,385,388,390,391,392,405,434],[180,309,373,381,385,388,390,391,392,405,434],[66,185,309,373,381,385,388,390,391,392,405,434],[80,309,373,381,385,388,390,391,392,405,434],[309,324,327,330,331,373,381,385,388,390,391,392,405,423,434],[309,327,373,381,385,388,390,391,392,405,410,423,434],[309,327,331,373,381,385,388,390,391,392,405,423,434],[309,373,381,385,388,390,391,392,405,410,434],[309,321,373,381,385,388,390,391,392,405,434],[309,325,373,381,385,388,390,391,392,405,434],[309,323,324,327,373,381,385,388,390,391,392,405,423,434],[309,373,381,385,388,390,391,392,394,405,420,434],[309,373,381,385,388,390,391,392,405,432,434],[309,321,373,381,385,388,390,391,392,405,432,434],[309,323,327,373,381,385,388,390,391,392,394,405,423,434],[309,318,319,320,322,326,373,381,384,385,388,390,391,392,405,410,423,434],[309,327,336,344,373,381,385,388,390,391,392,405,434],[309,319,325,373,381,385,388,390,391,392,405,434],[309,327,354,355,373,381,385,388,390,391,392,405,434],[309,319,322,327,373,381,385,388,390,391,392,405,414,423,432,434],[309,327,373,381,385,388,390,391,392,405,434],[309,323,327,373,381,385,388,390,391,392,405,423,434],[309,318,373,381,385,388,390,391,392,405,434],[309,321,322,323,325,326,327,328,329,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,355,356,357,358,359,373,381,385,388,390,391,392,405,434],[309,327,347,350,373,381,385,388,390,391,392,405,434],[309,327,336,337,338,373,381,385,388,390,391,392,405,434],[309,325,327,337,339,373,381,385,388,390,391,392,405,434],[309,326,373,381,385,388,390,391,392,405,434],[309,319,321,327,373,381,385,388,390,391,392,405,434],[309,327,331,337,339,373,381,385,388,390,391,392,405,434],[309,331,373,381,385,388,390,391,392,405,434],[309,325,327,330,373,381,385,388,390,391,392,405,423,434],[309,319,323,327,336,373,381,385,388,390,391,392,405,434],[309,327,347,373,381,385,388,390,391,392,405,434],[309,339,373,381,385,388,390,391,392,405,434],[309,319,323,327,331,373,381,385,388,390,391,392,405,434],[309,321,327,354,373,381,385,388,390,391,392,405,414,429,432,434],[309,373,381,385,388,390,391,392,405,434,472],[309,373,381,385,388,390,391,392,405,423,434,436,439,442,443],[309,373,381,385,388,390,391,392,405,410,423,434,439],[309,373,381,385,388,390,391,392,405,423,434,439,443],[309,373,381,385,388,390,391,392,405,433,434],[309,373,381,385,388,390,391,392,405,434,437],[309,373,381,385,388,390,391,392,405,423,434,435,436,439],[309,373,381,385,388,390,391,392,405,432,433,434],[309,373,381,385,388,390,391,392,394,405,423,434,435,439],[304,305,306,309,373,381,384,385,388,390,391,392,405,410,423,434,438],[309,373,381,385,388,390,391,392,405,434,439,448,456],[305,309,373,381,385,388,390,391,392,405,434,437],[309,373,381,385,388,390,391,392,405,434,439,466,467],[305,309,373,381,385,388,390,391,392,405,414,423,432,434,439],[309,373,381,385,388,390,391,392,405,434,439],[309,373,381,385,388,390,391,392,405,423,434,435,439],[304,309,373,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,405,433,434,435,437,438,439,440,441,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,467,468,469,470,471],[309,373,381,385,388,390,391,392,405,434,439,459,462],[309,373,381,385,388,390,391,392,405,434,439,448,449,450],[309,373,381,385,388,390,391,392,405,434,437,439,449,451],[309,373,381,385,388,390,391,392,405,434,438],[305,309,373,381,385,388,390,391,392,405,433,434,439],[309,373,381,385,388,390,391,392,405,434,439,443,449,451],[309,373,381,385,388,390,391,392,405,434,443],[309,373,381,385,388,390,391,392,405,423,434,437,439,442],[305,309,373,381,385,388,390,391,392,405,434,435,439,448],[309,373,381,385,388,390,391,392,405,434,439,459],[309,373,381,385,388,390,391,392,405,434,451],[305,309,373,381,385,388,390,391,392,405,434,435,439,443],[309,373,381,385,388,390,391,392,405,414,429,432,433,434,439,466],[284,309,373,381,385,388,390,391,392,405,434],[275,309,373,381,385,388,390,391,392,405,434],[275,278,309,373,381,385,388,390,391,392,405,434],[210,270,273,275,276,277,278,279,280,281,282,283,309,373,381,385,388,390,391,392,405,434],[206,208,278,309,373,381,385,388,390,391,392,405,434],[275,276,309,373,381,385,388,390,391,392,405,434],[207,275,277,309,373,381,385,388,390,391,392,405,434],[208,210,212,213,214,215,309,373,381,385,388,390,391,392,405,434],[210,212,214,215,309,373,381,385,388,390,391,392,405,434],[210,212,214,309,373,381,385,388,390,391,392,405,434],[207,210,212,213,215,309,373,381,385,388,390,391,392,405,434],[206,208,209,210,211,212,213,214,215,216,217,270,271,272,273,274,309,373,381,385,388,390,391,392,405,434],[206,208,209,212,309,373,381,385,388,390,391,392,405,434],[208,209,212,309,373,381,385,388,390,391,392,405,434],[212,215,309,373,381,385,388,390,391,392,405,434],[206,207,209,210,211,213,214,215,309,373,381,385,388,390,391,392,405,434],[206,207,208,212,275,309,373,381,385,388,390,391,392,405,434],[212,213,214,215,309,373,381,385,388,390,391,392,405,434],[214,309,373,381,385,388,390,391,392,405,434],[218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,309,373,381,385,388,390,391,392,405,434],[309,373,381,385,388,390,391,392,396,397,405,434],[309,373,374,378,381,385,388,390,391,392,397,405,434],[199,309,373,381,385,388,390,391,392,397,405,434],[309,373,374,381,385,388,390,391,392,405,434],[61,137,309,373,381,385,388,390,391,392,396,397,405,434,473],[60,195,303,309,373,381,385,388,390,391,392,405,434],[61,309,373,381,385,388,390,391,392,405,434],[61,199,309,373,381,385,388,390,391,392,397,405,434],[309,373,374,381,385,388,390,391,392,396,397,399,405,434],[198,200,309,373,381,385,388,390,391,392,397,405,434],[61,137,309,373,381,385,388,390,391,392,405,434],[285,286,309,373,381,385,388,390,391,392,405,434],[60,61,196,197,198,199,200,201,202,203,204,205,286,287,288,290,291,292,293,294,295,296,297,298,299,300,301,302,303,309,373,381,385,388,390,391,392,405,434,474],[204,309,373,374,381,385,388,390,391,392,397,405,434],[61,294,309,373,378,381,385,388,390,391,392,405,434],[60,62,63,198,303,309,373,374,381,385,388,390,391,392,396,397,405,423,434],[60,61,62,63,195,196,197,199,200,201,204,205,295,296,297,298,299,300,301,302,309,373,378,381,385,388,390,391,392,396,397,405,434],[60,309,373,381,385,388,390,391,392,396,397,405,434],[296,309,373,381,385,388,390,391,392,405,434],[198,203,204,296,309,373,374,378,381,385,388,390,391,392,396,397,405,434],[197,199,200,285,287,296,309,373,381,385,388,390,391,392,405,434],[202,205,286,288,290,291,292,293,295,309,373,381,385,388,390,391,392,405,434],[199,200,289,296,309,373,381,385,388,390,391,392,397,405,434],[285,287,296,309,373,381,385,388,390,391,392,405,434],[137,296,309,373,374,378,381,385,388,390,391,392,405,434],[199,200,285,287,296,309,373,381,385,388,390,391,392,405,434]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"d5c693c7241b88f974795daf08b61244f552f065ec93344f0a87bb2ea8b9c23c","signature":"9033850caf9a677b88e949247cc7bb10baf88436c87597478bb6e23eed521742"},{"version":"4743a6dba8d31255325d84ef0d5687997755332b5fe042c908bf56d8b22535cc","signature":"427af1572cfe2b3dd93c53285141b662f6ca5c5b16ee9a10b7aaeb6f523de244"},{"version":"a52c5f687d788d283ea1fa38bdc2fabe0eac863135a7dfe175ec52b309f61892","impliedFormat":1},{"version":"f2a60d253f7206372203b736144906bf135762100a2b3d1b415776ebf6575d07","impliedFormat":1},{"version":"86d4ff8ba66b5ea1df375fe6092d2b167682ccd5dd0d9b003a7d30d95a0cda32","impliedFormat":99},{"version":"652071821de280fae0ba53f0abcdd7350b1bda286f8fcd39394f5d4250de44ce","impliedFormat":99},{"version":"2b5368217b57528a60433558585186a925d9842fe64c1262adde8eac5cb8de33","impliedFormat":99},{"version":"e22273698b7aad4352f0eb3c981d510b5cf6b17fde2eeaa5c018bb065d15558f","impliedFormat":99},{"version":"0249cc57fb4f04fcc725481b5f273fe4a18d943e108724b216c762aaf311c255","impliedFormat":99},{"version":"c674b1e72c7f6879711bb0e920319b2760dacb7125fac4653702a1aa37a8b283","impliedFormat":99},{"version":"91c093343733c2c2d40bee28dc793eff3071af0cb53897651f8459ad25ad01da","impliedFormat":99},{"version":"6cc2be65d508f5404dae184fbe1bc5fc6287f2af93195feba921e619721f56a0","impliedFormat":99},{"version":"17c51065e7822de999ed5ff702aead6057c172067e485e8ffe9721bfe5010f0a","impliedFormat":99},{"version":"5c9a2aec7cf29c39450fe23930e192bb58a90b36c3f169481c3ef25f2fcf79e2","impliedFormat":99},{"version":"f4fc36916b3eac2ea0180532b46283808604e4b6ff11e5031494d05aa6661cc6","impliedFormat":99},{"version":"82e23a5d9f36ccdac5322227cd970a545b8c23179f2035388a1524f82f96d8d0","impliedFormat":99},{"version":"45160eb8c4c54610c5f43c103ff539e204d12aeed21c6cbe74c0fdf63741b5db","impliedFormat":99},{"version":"67cce3d39642e38f36602ab04d683b8a0c5e3a943eb03ac288e3d68c31596014","impliedFormat":99},{"version":"bfce32506c0d081212ff9d27ec466fa6135a695ba61d5a02738abd2442566231","impliedFormat":99},{"version":"ddaf5d3ddc45282b19fb0fecec91c87fc9b4d1f45c2ee611677345c81383c5c5","impliedFormat":99},{"version":"5668033966c8247576fc316629df131d6175d24ccf22940324c19c159671e1c1","impliedFormat":99},{"version":"7630b6a1c0ebaec2ef8e8abff850e1d6c551c47d1c345340a8ab95667460fc95","impliedFormat":99},{"version":"597b0a9ef02a28f5b1195305ec9f20a4f9948bd90ec3291d0343d1e5c0b4bd16","impliedFormat":99},{"version":"5c4081cb959a116933350a69585eff8a3eeb8c98c9b8fb0b9659a0ab51ab0be5","impliedFormat":99},{"version":"eb06d1cb283278811a37f17dc35047db62bcfa69e75f752ae170e318e4704f55","impliedFormat":99},{"version":"9bda3cb21c5022c86d2325885672085a8282a08c9df21688f7d3c6eff58efd40","impliedFormat":99},{"version":"e600e54a07ac7bcf9f0fd67722865bda454f5325ca4742e08e7c321a848fc5b0","impliedFormat":99},{"version":"ddd904d24dff387d2484b69e0643541102a0e3a4f750bb2d517f46adabf84bd7","impliedFormat":99},{"version":"045b2cdfaf5cf84f5be6bde21862add9503e104b252c6dc91efae422d4e8f975","impliedFormat":99},{"version":"19f21a767c89a4f3d8721ae698ce2a61b31d0809c1633d43c89b5aad30027b95","impliedFormat":99},{"version":"d2c24000d0dadcba27d36e6b6fed7abc2c6a9b3ed6b4a8e069be303812e86bba","impliedFormat":99},{"version":"ceaaae220b5495a8fbc15cb3925107a2b1c6f5a7ca1f4de63c439d91e7726f71","impliedFormat":99},{"version":"6496e6e04c8719315d51cb1c98452f8fcbab340a46cc859e94f3b3a5e2368ea9","impliedFormat":99},{"version":"1c521e08d75a9f4ec24e0ed84c7e7d7dfee4ab5e51aa9a4dbae76283a66d0c49","impliedFormat":99},{"version":"dbcde4d0b3a3fa5b64aaf3dc80370e76b00e8e8935a9bf1f4381a2e6299cd388","impliedFormat":99},{"version":"9242750c276f71a51c8d1ca11e4bc2df24ce1c988537c9c914fdd8ec8bb9715a","impliedFormat":99},{"version":"c4bb4f8d6dfff722a008022b3157c8e921b114d6befd8651add3028df40a00e4","impliedFormat":99},{"version":"a6d7338f7fd9035f468bc64f071edbf8ad8c6363c3bfb875c3e1c0f4f5f36b5f","impliedFormat":99},{"version":"d479a5591022bf7b46d90d10e23cbf004b2e1a30ad9b675dbd96721aafb78441","impliedFormat":99},{"version":"c7af3aec3d4a4607a9e23c63d802b77235dfe6f066f912c1347e06ce5868d919","impliedFormat":99},{"version":"d2e351dc6d967923dc6ff200bfbb20dfa92f0c23331ccf34eef1ea7d4b1d8e9b","impliedFormat":99},{"version":"0c8e28a077e369f1c26e00a3c2f76824c0ac7f55cfada1a6ff42f40b5eec0c98","impliedFormat":99},{"version":"bb8194f799c99acb6c3dc6b40866ef69cc2ce063f63666c9d1a55f6b4bb6f352","impliedFormat":99},{"version":"242cda707d18d4bd715c1916eed6ef3ab3d3c107bce17c771c803ff898dabd8c","impliedFormat":99},{"version":"7116b824716ea8c11ab831cb599e27cccd1ea083689d7085ce26954d86f392d4","impliedFormat":99},{"version":"25123c80bcb4c0f874ad6d2facf40f7cfd5e27c08a3107b18302307b7a131016","impliedFormat":99},{"version":"5fb5756d1c11073dcf4b2c76eecc0e48f0331bcb43e02ecb35a7c80f53bcb677","impliedFormat":99},{"version":"121cf1f1c42afca44d5aeedb4469d525ffeb013b78031c4a9089d64acb7a1394","impliedFormat":99},{"version":"908cb76e7cace05e1622e08912e2978c9284444ebedcdf8cd341fa75e31f007e","impliedFormat":99},{"version":"f93fe279ba4ac525dcc682b54923999538090ae9eecfeb0001b8009c70c15295","impliedFormat":99},{"version":"7e5217864cf444cb86d59472a218f6781f402c60436d823541360caa64c02244","impliedFormat":99},{"version":"37f61ebbaac9cc1bee0e3c11bf9a9b5207f8e1aa066eedb2860f108961987a49","impliedFormat":99},{"version":"263a89f026d661b338a22517a0375a19af55abb09651e73e9d7940025e2402bc","impliedFormat":99},{"version":"f9bf95954745207c3a305a59f3a8f7e36290c742d006d1ce447a41dc772ba3c3","impliedFormat":99},{"version":"732e1c24c3f5a76e61b075bfee7d2b3e5714d4960f8587b0cf989e7e151dc1ea","impliedFormat":99},{"version":"4cc5c2fb807317de6f88edae5cc2b24b705cdce764bbc1cc23aeec15d91a7a49","impliedFormat":99},{"version":"53cae4e7f0a5716f296870e5eef84af8832d5700b23ff79f349c0d1b4aa40d25","impliedFormat":99},{"version":"775e97f58cc774218eb4e979ff7f73b2fb4d958521df4707ae382b32fce5f55b","impliedFormat":99},{"version":"d93588a85b0b0eef4e6ab906fa37caa21efa1d30647aef292567c078b2e3a0a9","impliedFormat":99},{"version":"2eaf0dcaaa03f1cce8c4069c98d198b4730d6e842d393031328aefd1ed7becb1","impliedFormat":99},{"version":"d62b09cb6f1ceb87ec6c26f3789bc38f8be9fb0ce3126fd0bf89b003d0cba371","impliedFormat":99},{"version":"4a5d9348012a3e46c03888e71b0d318cda7e7db25869731375f90edad8dcea02","impliedFormat":99},{"version":"61b3add3d48dfc79324531ede7da59203059a62986070f97645a83acd3f20aa0","impliedFormat":99},{"version":"6cd8356a92fd9f1edcbfbd3b891f50228738522e79bfdad16e7fb7cfd4a66932","impliedFormat":99},{"version":"347efb60859c806ef954a67ee7520c9aa33e1881eedd40d236298af775deef50","impliedFormat":99},{"version":"fc391876e409d362cc43a7468226a9eb83440de09873b284bf09fbfb261ec259","impliedFormat":99},{"version":"d06f5012d5ac1bc25c5033f7e916fe42cc0253d6b523b9747809b71676069370","impliedFormat":99},{"version":"5d35840bd540fad886e21ddaf9b078a44c21a827dec9abc08d2d2c1a3ff27d44","impliedFormat":99},{"version":"a02182b20bcb1966fc15eac80506f617b71fdd0e279ccff44b27f2ee366b2823","impliedFormat":99},{"version":"32563899782c456f03cadc7a9508b9b6468dd678404b093bd7557d6c6e143218","impliedFormat":99},{"version":"f613a93e0685802f7f7e248156ae93ff9088d45abeff0b21b656520699b79f06","impliedFormat":99},{"version":"5471b59fcb6ad04c41f6bf57075e88f3094d9d498e51595b4341d8bfcb729bf5","impliedFormat":99},{"version":"6aeb85043e6a5d2c3768c413a01885b0fc3dbfb4b3817fde5bb93601f5efb303","impliedFormat":99},{"version":"b6ff37737d006b86082f2f7176eb0a771001e9dde9152a26ef9ea8fd80e6eba0","impliedFormat":99},{"version":"491d5f012b1de793c45e75a930f5cdef1ff0e7875968e743fa6bd5dd7d31cb3b","impliedFormat":99},{"version":"1fd56873ada3f2bf6049ae741cf4efc1c90693015a6dc3467d6c995e5a6db03a","impliedFormat":99},{"version":"8074e5e85360339cc57e7a9b6db5f49af9ef8d6d0ee3c28a02d5e0ad5c21920b","impliedFormat":99},{"version":"43db9ade57eeeb241749f460e5eec4cc4eca8e8f3c35a0542a5a746eb2bcaf86","impliedFormat":99},{"version":"53c86b81daa463deacb0046fee490b6d589438ac71311050b74dcee99afca0f6","impliedFormat":99},{"version":"70587241a4cc2e08ffc30e60c20f3eb38bd5af7e3d99640568ffe2993f933485","impliedFormat":99},{"version":"dd01943d0fe191b3b2020438367709333ff08a69d285e2f715a60711dcf83b61","impliedFormat":99},{"version":"9c7188dc07bc5ce82bbe5b75495f44a7887b0a4945f63696661fffeddca0c0e9","impliedFormat":99},{"version":"93ea079d0b9af94efc1578a95aa0299ec4054f617fb31d243f66255e221276fd","impliedFormat":99},{"version":"4ecb0eb653de7093f2eb589cea5b35fdea6e2bbd62bc3d9fafdc5702850f7714","impliedFormat":99},{"version":"69ed52603ad6430aaffbc9dec25e0d01df733aaa32ab4d57d37987aedc94c349","impliedFormat":99},{"version":"6f8acb191da449d8dbec7a4e9c317bdb6b8af104a60a101950643ea52cfa3c85","impliedFormat":99},{"version":"4c01241847f841eddf3d727aac5686d8d1e06c92124002de9d9ed2ad3c590420","impliedFormat":99},{"version":"8bba80ef1e0e9ae8c061728626309824023e85eaafcd8c285a6fa89dc6881573","impliedFormat":99},{"version":"ada6bd808581a783390b1aabc2cc836136a5d214af0d924cc57d9f29b5733ce9","impliedFormat":99},{"version":"283336202f1a6a4e13271dc83b776718cf5d4a4137b28e2d013498e3020f7170","impliedFormat":99},{"version":"54a6a3e98b7ec00fec7bd7e42ad50c16014805576ccbe33bfee04f0aac9965da","impliedFormat":99},{"version":"7c90a7108c4319b0475d5419d52f2a2c9bf499234a2a15d5b8504983e141041b","impliedFormat":99},{"version":"67fc5d1b6877a799de1e3943ed2c3669b72a6ab3b17c7b0b0387bdd6e4c1a01f","impliedFormat":99},{"version":"8ac25d431d9b1bbe3ded6c578651cc43acbdbf19c435fbbe185b827ae74ba3df","impliedFormat":99},{"version":"953ee863def1b11f321dcb17a7a91686aa582e69dd4ec370e9e33fbad2adcfd3","impliedFormat":99},{"version":"392e72d77ae33ee322d5b0b907398f2200f72d36adaca1ca62dfa7e22f744ac3","impliedFormat":99},{"version":"e452b617664fc3d2db96f64ef3addadb8c1ef275eff7946373528b1d6c86a217","impliedFormat":99},{"version":"c6a811837fef3d4ba22e7e4adcb16f12caf30252047b133404d698bf8f0e883a","impliedFormat":99},{"version":"2f722a3a421baf9a7c175d8ae6a3118dfd14c5f36474e03f99e3df5800065030","impliedFormat":99},{"version":"f9511d2a891b0a017ae31674977b053f42ca7221dedd012f6de6f75e7cb9aa3e","impliedFormat":99},{"version":"d8f262b549f3ed95402297d10b84f0f86e3113d6d570b03364d2cfca1f75e5d8","impliedFormat":99},{"version":"f216cb46ebeff3f767183626f70d18242307b2c3aab203841ae1d309277aad6b","impliedFormat":99},{"version":"d6d95f96dd5b374484fd000228288cbcfb80aa47cb74ebd3e19ea94a36e8260a","impliedFormat":99},{"version":"9abda1f0836e696725c31ea63d36a6c7c54e0f762d5e387f52b27186dff81cab","impliedFormat":99},{"version":"92fb8aa5d61dca9ab2008d49397a639dbf71c7746da23c02245523cfec4a99ef","impliedFormat":99},{"version":"9e6cd6dc690d6e6c89b17b295cabf8a5a08011ae79a7a56578a429e5ae27b8dc","impliedFormat":99},{"version":"4c7eafc682ffbd45ac24d056c63a622993048ac272c76ba1721118dc601ab629","impliedFormat":99},{"version":"f72b0af7e81183c17d799cdea2ab0d81580dfe96a98343a21d746984c3b21933","impliedFormat":99},{"version":"3841ca1577c0927f59fac8faa7cd195485c5362d99ec2b16ff9b86ec4974a3e5","impliedFormat":99},{"version":"58c5a2a520ae555e0573873a5e6303b0f1a1e70f3b376e5ac9094eaad0623d8a","impliedFormat":99},{"version":"5f8217240c95e3f3007d9968104904616287f30d853bac73874759c1dfad4017","impliedFormat":99},{"version":"7ebc96af203f866e829b528e5cffb32111a1a1ff4662bc60c3b53696e89c67f4","impliedFormat":99},{"version":"9f5ee7c037b58964c1cee63c1849fa11757f693208444be0f2d9f08defe859cd","impliedFormat":99},{"version":"33a4085365aa21a995ea4721ffff814128b126e8e346e5f064d87bfcdd0ff7ce","impliedFormat":99},{"version":"3adf214b4b307152af85b77e441d36ede388dadba2bd9962671bf933738d2a25","impliedFormat":99},{"version":"9a2cc98a7884cb530a704f6cd16a83db9aa89360a2b391a49e498b5179443dc6","impliedFormat":99},{"version":"250998ae18ea49b8745d327e7739f56464a4318783129daab90b3299bf6f8a55","impliedFormat":99},{"version":"76b3afd1f2748ff725c277bd4701f442af697c0586e1b491e6a67383a246ffad","impliedFormat":99},{"version":"4df5fc6fc2438b8e3418cb25c8c0e863d1f92e4470297d6a8756394c597af844","impliedFormat":99},{"version":"92b5f0879161f1206e30a0c219dd8f23d736f2a74a4e015885e8e3f3b3c9a3e7","impliedFormat":99},{"version":"374d12016302e312ffccd3d38e6f3df1b412378bff6e6266f3e5844af450859c","impliedFormat":99},{"version":"18d0c2293aa57e33923fc1b10970650c6d6932dbfa711a3ffd67600b3caf924b","impliedFormat":99},{"version":"17758b72f880ed66754e3ff4aeade0b82417ec546b72bf3a326cadf4e56c1915","impliedFormat":99},{"version":"ffa547cbf7599d89b6ac4c2f038f99978e0dff46bb9850df46168ce6809a5b24","impliedFormat":99},{"version":"981240d6d3015e8de441325b90c07588a302f2d9c377bccfc61e4680b726f962","impliedFormat":99},{"version":"493c39c5f9e9c050c10930448fda1be8de10a0d9b34dcd24ff17a1713c282162","impliedFormat":99},{"version":"73e4673f2da8677556210e5a127b2637bf030ab73da222ea2a19979f89d9d40a","impliedFormat":99},{"version":"e9d27f2b7d5171f512053f153cadc303d1b84d00c98e917664ba68eca9b7af6a","impliedFormat":99},{"version":"4899d2cf406cd68748c5d536b736c90339a39f996945126d8a11355eba5f56f3","impliedFormat":99},{"version":"29c4e9ce50026f15c4e58637d8668ced90f82ce7605ca2fd7b521667caa4a12c","impliedFormat":99},{"version":"8575340c8560a52c3309956add745660ad319dbd67309fa268f5af9b1c7551f5","impliedFormat":99},{"version":"3b56bc74e48ec8704af54db1f6ecfee746297ee344b12e990ba5f406431014c1","impliedFormat":99},{"version":"9e4991da8b398fa3ee9b889b272b4fe3c21e898d873916b89c641c0717caed10","impliedFormat":99},{"version":"35290a0ba8d9287d4f3635948e7e84bcf14223239ad07902d111684dfafffaf4","impliedFormat":99},{"version":"dbf3d90c21c08217509df631336881a3105740033b0592dcc47036490f95e51c","impliedFormat":99},{"version":"e6ad9376e7d088ce1dc6d3183ba5f0b3fb67ee586aa824cc8519b52f2341307a","impliedFormat":99},{"version":"2391f2f7e3819f74e51039e431d4d68dd261e40e677810316008f08e778492e8","signature":"7446741158fcd0894addb90d6f00769719782745e713ad5b201eb0132cfb6551"},{"version":"0ca51b5dc00aa2c6a3551c5b511e7ca9a1a08e12d25422bd974c2ce1707ff959","signature":"399852f50bf374c4e4c8efb4274260a0b29c3ec863e71d6a754d69818276eb59"},{"version":"ad35da447be046219c92634332ceb694b52dbc37eed13536fd18bd802ff4ed53","signature":"16d5bdedef7ac1bafc90ef5edcb5a524f7a067285e3c68b742c3def1dc8a0909"},{"version":"5be00abdcbb4485dbe416d5301421f7086cca3689dadb5ece2a53676c1d17623","signature":"9693c11eef0516a468debb1a89eaa23472061a106a7586e3465fc840909e8f03"},{"version":"91243a5cf61d442103e43100007078aaf4634840971462b7df19faa687ece79a","signature":"4e01fdfcfa9d19e0f9522f61174233fad5db2988f030045fa80a73e32dfca9fd"},{"version":"750adbd023894fa6e7af9b6eddecf7de74b5e4b537ad46f220879eb937b8244a","signature":"f24da613e5429ab6caa9687e57745be0bd9e54d5760a66f3a116f2988a79f001"},{"version":"1e693a098c4d53ddd27d499ca4d8b3ee935663aa191e62d50eeff08b26146532","signature":"99662eb2cba010fdf1202a3e222c89b8492ff68e8e206a2296f6c6ec3ee6608c"},{"version":"fe8fcf43c4614dc877d3e56d518e95d69defa07f357b2af4c385b5fb1c591929","signature":"4c0b88ca9250fa0e0fa0ce791c837487fe6b100992833f19555048cb1b47eb18"},{"version":"324df572195e93def72d9867f75dc80446d6bbaf2c9a9f0f3a45fe5b5bd8af21","signature":"dc91ed21005509f86ef528d8c3bc026e5829206e558b30ac479c6109ff0ceee4"},{"version":"f2867aef276fc96e82476403721c7cc0725df14a4e47351b93be31a28da8a7c0","signature":"a8721a1ee15a172d5d0221d47f3f1746879713898dbd7f40f3ec95fdfd379f2c"},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"ead8e39c2e11891f286b06ae2aa71f208b1802661fcdb2425cffa4f494a68854","impliedFormat":1},{"version":"40ba6c32eb732a09e4446ade5cb6ad0c147f186f9c9dc6878b90b4418ad9f6ea","impliedFormat":1},{"version":"fdd814741843f85c98281522c58f5a646590ba9019fad2efaa95987655e0611b","impliedFormat":1},{"version":"c78aff4fb58b28b8f642d5095fc7eeb79f00e652a67caa19693af1adabb833c9","impliedFormat":1},{"version":"f80a08ced8818dc99359c0acd5b3f12762e1ce53758007759b0d4e503cbf4a5e","impliedFormat":1},{"version":"37935fa7564bcc6e0bc845b766a24391098d26f7c8245d6e8ab37bc016816e94","impliedFormat":1},{"version":"68add36d9632bc096d7245d24d6b0b8ad5f125183016102a3dad4c9c2438ccb0","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"0f8a263f4c8595c8a07de52e3f3927640c44386c1aa2984de9eae50d75e613b2","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"346fffde7c32da87c2196eb7494422449dc2ca82d3b4e6bf55be1d1a33ffc2b0","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8b5875e4958528042103fdd775e106a7f76bafc29709f0690df9a7d2241d52a7","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"d8430c275b0f59417ea8e173cfb888a4477b430ec35b595bf734f3ec7a7d729f","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"aef26cf95593c8ace1c62c4724f9afac77bdfa756fb8a00613cd152117cb2f43","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"268fd6d9f2e807a39a6c5aa654b00f949feb63d3faa7dd0f9bba7dde9172159c","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},{"version":"dc9cc4abaabd5cfd8d2749c9478d0abc7055e1b1fd922dff872e6e95a4175201","signature":"2651a6c04c20c6b829234047caa3db2f1b697af6365b3387872c3f5700dc28a4"},{"version":"12262a57c1c71f8d1bc50d5508ab3c53ad4afd89fd29d97e27ebf46035b3da3e","signature":"c4d6f8fba3839bb1e5ee491b43906b9183d7b8959baebd3ec5913fef4f40b635"},{"version":"decbb658d7b354b7fdc59db5ab541e6327b20ec00c96e891769e50f77d925523","signature":"be8b6bee4b407e0540537b69d6a4874eaf930d6a8b70ecc065c796fd1437c846"},{"version":"8b61608c154f87e13c88b21b5208a3acb906ddcee5e3001f3b27ef13703b61e8","impliedFormat":1},{"version":"f6b6e3d4ec6532a05ccad69c2605b792e7473039b9f16261a199b9dcf1c33084","signature":"c3196bfd12c96e513ae6baf5c945b3bfbaa2e8695693b3628414fc732cbe9fab"},{"version":"ab30e3e6030f247c9ead0b2e6928001e1be20907637719b3559c5da617002798","signature":"8fa424847eae1e9607dad437ba29f73de7ed8a64b60f26feddf422f3e9656655"},{"version":"b39529dc70f822883c8805eba3be4b896603b8be5c21fa9a5e8456853a9cac24","signature":"11b670ed51a4a4a6847d013b9dc50601581fa09816ba6d060c8f13a94df71c3e"},{"version":"7c45a0cff6182af8c36f68d2a3523c0d74c9019b32cd0aa071e236e774b67a71","signature":"fc6cb9ca05da11c96a920ca17e25d9aef74a7fe8e523b8046c5cea481255aeec"},{"version":"e8a7565fa8cc70df160ab9ffcab5098d0cf1b3e646ed266be197f0eacc857b34","signature":"8c4244dae03efb90eb6008beb0eec636d1d5bd8576d80a7c39a4c63e389e88ee"},{"version":"4e63aa0af8005027ec3e47a329ba2bce4ad30a7866f33fa3bc9f13a702022770","signature":"ee324efc620efe75b8389fb704c27a77e6dd246ef3cb238f1151f9768de30764"},{"version":"e6c8c2edceaf3627f7816caf15dcf6393e444ba776e8902e5f65b74d451b5cc6","signature":"d7e0473f2a6a61a3ecd75e7e79efac2edb2df3c3821a437b607d0757da48bd69"},{"version":"a23023a5093a3f8a871a80c958c0948b4bb98bf0a29972758b3c902f830457a2","signature":"9ebfe9eb079314c554dc22cec3fe0071c22226c5cffebfc4308ad406883466dc"},{"version":"952c21e8d1e308d08bf902e415daba0eeb927e60321d023e94c15cb2e211d043","signature":"afdb5288a1739d52d50c117da8b6caedb777042a5fa11f02ade7b4a5bf72d224"},{"version":"6e80e675b53df355531159abae68e983264ed2364f078e27edee37df7bd051da","signature":"a6afd1c9a3668eb51fc397e00dec8dc655aa4dc52479d5dfa95afd821e9b16f6"},{"version":"e2b671779a5c5304390d08a28d3f96338e05e418c4c18624ada291261c1a4d56","signature":"b985c9126287c3b7342fd7316a8a2899cf50d3996dff2cc901000965c9f19b8f"},{"version":"71ad1cc06270386d90b3c21b1bf1f99d65cb8a31851e8e912eb68193cf60e766","signature":"a3d4dd864c61c246a59b05bce5d84c3d219567f85f9a32e539c4a2be8dd58029"},{"version":"485e4b9a91ec3ec31512ee4d7b9aaa7aaac2886bb4770c8022f9f3e20a6e1a03","signature":"17f679537f92a306af086a40d952e8a3cf2c5f99e1fd05eb6674babe31b54ed5"},{"version":"4623d7cf72b29b7f5b223b97cb8618f9c11a6013f9e3ff6519889a423994519b","signature":"bd9508d745eb868afbc50c1a262379fd19b8c8e467c21612b246b8c3b84a4d7b"},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"0ecd58f413f9bc3b7d4383eae31b0c8fc576985cd7404d6f99f8c643543ade74","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"c8b8968311ec4e5e97b7b5fb8a65efaba455db9bdcfd7fff7fb15f6e317bfba0","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"aecbf1d9e6a18dab7d92ef8a89a1444b47e1eb6134cb2bb776a26d55ff58c29a","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"1fede9296beac11ce8e6b425396a1791f64341f2be85deebb6286faf6e16306e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce697b6a251d9cad53998c7fd3098072df883b525ec45d83530e434dc6d80dc6","impliedFormat":1},{"version":"719412f054e6ecc35489462c9a21bab0323d173a7d04e55b0ace4b5d86fbeb07","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"3db996ecdee7aabecc5385976cc07eb66216034a273c07b17d1a85292e9bab0c","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"5ff4433a2deae4f85ab1377e90a7554ce6b47ae51c69a84ca30a6e22fae85834","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"c1fa52b3d014001e8662fa2669d90ea15373958a288e3b83a3b621733d25292a","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"5e66972e83eb4dc7123939bf816e6cbd9ad81af5552db1cab84e6bd9c64d2ecc","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"8413d0641f293aed551c7464615b770d34a02dedede889b9591172287d68e773","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"1b241e24f3227d078c06aeda6e050187ad59a4e591f4467abed44d92b084e08d","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"7fde0e1be5c8be204ffbf428abfcf01da2eb0f130e1bc3f539eb7275f4fd1f58","impliedFormat":1},{"version":"e284328553df5f425a5d33d36a0c3fa66b46af9d097cad6f4d2e8696dfdeb0f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"f36b3fbe2be150a9ca140da48593f21e6a8172004f92ddc549b43efec39f3e54","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"016b29bf4926b80255a108c53a1451717350059da04fcae64d1075f5e93bbb39","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"1c4f139ade4f6ebf45463505f8155173e5d7a5305e50e0aae0a5e712d6ff3b48","impliedFormat":1},{"version":"e16b319e5aca1031168de823c4946ff8e29629c4c8cc0ec0fcfe2a8ab2155043","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"2c3c5c0f54055e87640f5d233716fd889f3034fc7911d603b642369b0dbeb2a7","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"1c9e5b1a17b1fc9b3711fb36e0690421261ab2880f15b145155b5b2ba2ab6c2d","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"afa1c49f8e559e413d57343339db857d2a8159435cf9cf7d4deb41718fff1b88","impliedFormat":1},{"version":"6953d7597831d0860c7034cf4f0419687d263b6b98a4b32e37ce6d49615c36e2","impliedFormat":1},{"version":"3ac40516c33b87f751f7507346933081a26cdb8a3e11a6b3aa07d23f803c85db","impliedFormat":1},{"version":"4ac80270b6787c2b77a2d98a9714a71f4363c24b5890314f3ba582c94bfbe779","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"171b96f31e3fbdb55fe570f2a29a5ee47223fdca95a84ea2142e4cc4feaf9dfe","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"d243db6b25788f439e7e2f03c05688e92f46764351673bb0e7b2f3631232e186","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"b3fb72492a07a76f7bfa29ecadd029eea081df11512e4dfe6f930a5a9cb1fb75","impliedFormat":1},{"version":"cd6db591e858b53f23faad85346a4f43732bb33d6dff5f3f4dc47c4a448804ae","signature":"ad2664f941e8db7e19f9bae8100db326d4d1ed5701d56c4579331e575a43718e"},{"version":"328b1b71f3bedfca3331d6253b6f89aa207d876f1486a1017beebfa39fc8d418","signature":"47b10209e6f074245affea091a75da917d59c0e9bee84a78618c3a46359dc229"}],"root":[60,61,[196,205],[286,288],[290,303],474,475],"options":{"composite":true,"declaration":true,"declarationMap":true,"esModuleInterop":true,"module":99,"outDir":"./dist","rootDir":"./src","skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[63,1],[370,2],[371,2],[372,3],[309,4],[373,5],[374,6],[375,7],[307,1],[376,8],[377,9],[378,10],[379,11],[380,12],[381,13],[382,13],[383,14],[384,15],[385,16],[386,17],[310,1],[308,1],[387,18],[388,19],[389,20],[432,21],[390,22],[391,23],[392,22],[393,24],[394,25],[396,26],[397,27],[398,27],[399,27],[400,28],[401,29],[402,30],[403,31],[404,32],[405,33],[406,33],[407,34],[408,1],[409,1],[410,35],[411,36],[412,37],[413,35],[414,38],[415,39],[416,40],[417,41],[418,42],[419,43],[420,44],[421,45],[422,46],[423,47],[424,48],[425,49],[426,50],[427,51],[428,52],[311,22],[312,1],[313,53],[314,54],[315,1],[316,55],[317,1],[361,56],[362,57],[363,58],[364,58],[365,59],[366,1],[367,5],[368,60],[369,57],[429,61],[430,62],[431,63],[395,1],[62,1],[77,1],[135,64],[136,65],[184,66],[75,67],[72,68],[76,69],[185,70],[68,71],[80,72],[119,73],[137,74],[64,1],[66,1],[74,75],[69,76],[67,5],[79,77],[65,1],[78,78],[70,79],[187,80],[121,81],[192,82],[188,83],[189,84],[120,85],[190,1],[133,86],[138,87],[139,88],[134,89],[191,90],[73,91],[111,92],[83,93],[84,94],[85,93],[86,95],[93,96],[91,97],[92,93],[87,93],[110,98],[94,93],[95,94],[96,95],[104,99],[103,97],[97,95],[98,95],[109,100],[99,93],[100,97],[101,93],[102,93],[107,101],[108,102],[88,93],[89,93],[90,95],[105,101],[106,103],[115,104],[112,95],[113,105],[114,106],[116,107],[124,108],[131,109],[130,110],[129,111],[128,112],[127,113],[125,95],[126,95],[117,114],[122,115],[118,116],[123,117],[81,118],[195,119],[193,120],[194,121],[186,122],[82,123],[132,124],[145,125],[143,95],[144,126],[147,127],[146,128],[148,95],[152,129],[150,130],[151,131],[153,132],[156,133],[155,134],[158,135],[157,93],[161,136],[159,94],[160,137],[154,138],[149,139],[162,138],[163,140],[183,141],[164,93],[165,95],[166,142],[167,143],[168,144],[140,145],[141,146],[142,147],[71,1],[169,95],[172,148],[170,95],[171,149],[173,150],[174,151],[177,152],[176,153],[178,154],[179,132],[182,155],[181,156],[180,157],[175,158],[58,1],[59,1],[11,1],[10,1],[2,1],[12,1],[13,1],[14,1],[15,1],[16,1],[17,1],[18,1],[19,1],[3,1],[20,1],[21,1],[4,1],[22,1],[26,1],[23,1],[24,1],[25,1],[27,1],[28,1],[29,1],[5,1],[30,1],[31,1],[32,1],[33,1],[6,1],[37,1],[34,1],[35,1],[36,1],[38,1],[7,1],[39,1],[44,1],[45,1],[40,1],[41,1],[42,1],[43,1],[8,1],[49,1],[46,1],[47,1],[48,1],[50,1],[9,1],[51,1],[52,1],[53,1],[55,1],[54,1],[56,1],[1,1],[57,1],[336,159],[349,160],[333,161],[350,162],[359,163],[324,164],[325,165],[323,166],[358,167],[353,168],[357,169],[327,170],[346,171],[326,172],[356,173],[321,174],[322,168],[328,175],[329,1],[335,176],[332,175],[319,177],[360,178],[351,179],[339,180],[338,175],[340,181],[343,182],[337,183],[341,184],[354,167],[330,185],[331,186],[344,187],[320,162],[348,188],[347,175],[334,186],[342,189],[345,190],[352,1],[318,1],[355,191],[473,192],[448,193],[461,194],[445,195],[462,162],[471,196],[436,197],[437,198],[435,166],[470,167],[465,199],[469,200],[439,201],[458,202],[438,203],[468,204],[433,205],[434,199],[440,206],[441,1],[447,207],[444,206],[305,208],[472,209],[463,210],[451,211],[450,206],[452,212],[455,213],[449,214],[453,215],[466,167],[442,216],[443,217],[456,218],[306,162],[460,219],[459,206],[446,217],[454,220],[457,221],[464,1],[304,1],[467,222],[285,223],[279,224],[283,225],[280,225],[276,224],[284,226],[281,227],[282,225],[277,228],[278,229],[272,230],[213,231],[215,232],[271,1],[214,233],[275,234],[274,235],[273,236],[206,1],[216,231],[217,1],[208,237],[212,238],[207,1],[209,239],[210,240],[211,1],[218,241],[219,241],[220,241],[221,241],[222,241],[223,241],[224,241],[225,241],[226,241],[227,241],[228,241],[229,241],[230,241],[231,241],[233,241],[232,241],[234,241],[235,241],[236,241],[237,241],[238,241],[270,242],[239,241],[240,241],[241,241],[242,241],[243,241],[244,241],[245,241],[246,241],[247,241],[248,241],[249,241],[250,241],[251,241],[253,241],[252,241],[254,241],[255,241],[256,241],[257,241],[258,241],[259,241],[260,241],[261,241],[262,241],[263,241],[264,241],[265,241],[266,241],[269,241],[267,241],[268,241],[289,1],[203,1],[298,243],[297,243],[299,244],[200,245],[60,1],[196,246],[474,247],[302,248],[197,249],[300,250],[204,246],[198,251],[199,252],[301,1],[286,253],[287,254],[475,255],[294,256],[295,257],[201,258],[303,259],[61,260],[202,261],[205,262],[288,263],[296,264],[290,265],[291,266],[292,267],[293,268]],"latestChangedDtsFile":"./dist/tools/write-handler.d.ts","version":"6.0.3"} \ No newline at end of file From ca9d683b7903fe31b8036e5241614ef1dc571c08 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 17 Jun 2026 17:33:44 +0800 Subject: [PATCH 173/212] =?UTF-8?q?chore(git):=20=E6=9B=B4=E6=96=B0.gitign?= =?UTF-8?q?ore=E4=BB=A5=E5=BF=BD=E7=95=A5=E6=9B=B4=E5=A4=9A=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了操作系统元数据文件的忽略规则(.DS_Store, Thumbs.db) - 新增对构建产物目录dist/和out/的忽略 - 添加了编辑器相关的忽略配置(.idea/, .vscode/) - 保留对node_modules/目录的忽略规则 - 移除了重复的.DS_Store忽略项 --- .gitignore | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index cceec9f7..c2546449 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,15 @@ +# OS metadata +.DS_Store +Thumbs.db + +# Dependency directory node_modules/ +# Ignore built ts files dist/ out/ -src/generated/ -.DS_Store + + +# Editors .idea/ .vscode/ *.tgz From a7949c325fac4fe59191731b9027ec8e10d9ef43 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 17 Jun 2026 17:35:15 +0800 Subject: [PATCH 174/212] =?UTF-8?q?chore(config):=20=E9=85=8D=E7=BD=AEnpm?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=E5=92=8CNode=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增.npmrc文件,指定npm注册表地址为https://registry.npmjs.org - 新增.nvmrc文件,指定Node.js版本为22 --- .npmrc | 1 + .nvmrc | 1 + 2 files changed, 2 insertions(+) create mode 100644 .npmrc create mode 100644 .nvmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..38f11c64 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 From 499081ababcfbe54ddab01e412e89a2bcc5cd8d3 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 18 Jun 2026 10:18:49 +0800 Subject: [PATCH 175/212] =?UTF-8?q?build(vscode-ide-companion):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=A8=A1=E6=9D=BF=E6=96=87=E4=BB=B6=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E6=94=AF=E6=8C=81=E5=B9=B6=E6=9B=B4=E6=96=B0=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 build 脚本中新增复制 core 包 templates 到 vscode-ide-companion/templates - 修正 .gitignore,忽略 vscode-ide-companion 内的 templates 目录 - vscode-ide-companion package.json 中新增 templates 目录到文件列表 - 新增 scripts/prepare-package.js,实现构建、检查、发布、Git 提交与打 tag - 新增 scripts/version.js,实现 monorepo 中多个包版本统一更新与 lockfile 重生成 - package.json 新增 release:version 和 prepare:package 脚本命令 - 添加 RELEASE.md 及 RELEASE_en.md,详细说明版本发布和发布流程文档 --- .gitignore | 3 +- RELEASE.md | 235 ++++++++++++++++ RELEASE_en.md | 235 ++++++++++++++++ package.json | 4 +- packages/vscode-ide-companion/package.json | 1 + scripts/build-vscode-companion.js | 21 +- scripts/prepare-package.js | 302 +++++++++++++++++++++ scripts/version.js | 296 ++++++++++++++++++++ 8 files changed, 1092 insertions(+), 5 deletions(-) create mode 100644 RELEASE.md create mode 100644 RELEASE_en.md create mode 100644 scripts/prepare-package.js create mode 100644 scripts/version.js diff --git a/.gitignore b/.gitignore index c2546449..37f64ba0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ out/ # Generated files packages/cli/src/generated/ packages/core/src/generated/ -packages/vscode-ide-companion/*.vsix \ No newline at end of file +packages/vscode-ide-companion/*.vsix +packages/vscode-ide-companion/templates/ \ No newline at end of file diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..deca0fd7 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,235 @@ +# 版本发布 + +Deep Code 使用两个脚本管理 monorepo 的版本发布流程: + +| 脚本 | 命令 | 用途 | +|------|------|------| +| `scripts/version.js` | `npm run release:version` | 升级所有 workspace 包的版本号 + 重新生成 lockfile | +| `scripts/prepare-package.js` | `npm run prepare:package` | 构建 + 质量检查 + 发布到 npm + git commit & tag | + +两者配合使用,先升版本号,再发布。 + +--- + +## release:version — 版本号升级 + +用法与 `npm version` 一致,支持所有标准 bump 类型。 + +### 基本用法 + +```bash +npm run release:version -- [options] +``` + +> 注意:npm scripts 传参需要 `--` 分隔符。 + +### 支持的 bump 类型 + +| 类型 | 当前版本 | 结果 | 说明 | +|------|---------|------|------| +| `patch` | `0.1.31` | `0.1.32` | 补丁版本 +1 | +| `minor` | `0.1.31` | `0.2.0` | 次版本 +1,patch 归零 | +| `major` | `0.1.31` | `1.0.0` | 主版本 +1,minor/patch 归零 | +| `prepatch` | `0.1.31` | `0.1.32-0` | 预发布补丁 | +| `preminor` | `0.1.31` | `0.2.0-0` | 预发布次版本 | +| `premajor` | `0.1.31` | `1.0.0-0` | 预发布主版本 | +| `prerelease` | `0.1.31` | `0.1.32-0` | 递增预发布号 | +| `from-git` | — | 从最新 git tag 读取 | 适用于已有 tag 但未更新 package.json 的情况 | + +也可以直接指定版本号: + +```bash +npm run release:version -- 0.2.0 +``` + +### 预发布链 + +`prerelease` 支持链式递增: + +``` +0.1.31 + → prerelease → 0.1.32-beta.0 + → prerelease → 0.1.32-beta.1 + → prerelease → 0.1.32-beta.2 + → patch → 0.1.32 (去掉 prerelease 后缀) +``` + +### --preid 选项 + +预发布标识符,默认为 `"0"`,可自定义: + +```bash +npm run release:version -- prerelease --preid beta +# 0.1.31 → 0.1.32-beta.0 + +npm run release:version -- premajor --preid alpha +# 0.1.31 → 1.0.0-alpha.0 +``` + +### 实际执行的操作 + +1. 读取 `packages/core/package.json` 中的当前版本 +2. 根据 bump 类型计算目标版本 +3. 更新 **所有** `packages/*/package.json` 的 `version` 字段(core、cli、vscode-ide-companion) +4. 删除旧的 `package-lock.json`,执行 `npm install --package-lock-only` 重新生成 + +### 完整示例 + +```bash +# 升级 patch 版本 +npm run release:version -- patch + +# 升级 minor 版本 +npm run release:version -- minor + +# 发布 beta 预发布版 +npm run release:version -- prerelease --preid beta + +# 直接指定版本 +npm run release:version -- 0.2.0 + +# 从 git tag 获取版本 +npm run release:version -- from-git +``` + +升级后检查变更,确认无误后提交: + +```bash +git diff +git add -A +git commit -m "chore(release): v0.1.32" +git tag v0.1.32 +``` + +--- + +## prepare:package — 构建并发布到 npm + +完成质量检查、构建、发布两个 npm 包,并自动创建 git commit 和 tag。 + +### 基本用法 + +```bash +npm run prepare:package -- [options] +``` + +### 参数 + +| 参数 | 说明 | +|------|------| +| `` | **必填**,要发布的 semver 版本号 | +| `--tag ` | npm dist-tag,默认 `"latest`",常用于 `beta`、`next` | +| `--dry-run` | 预演模式,不实际执行任何写操作 | +| `--force` | 跳过 main 分支检查,允许从其他分支发布 | + +### 执行流程(9 步) + +| 步骤 | 操作 | 说明 | +|------|------|------| +| 1 | Git 检查 | 工作区必须 clean,必须在 main 分支(`--force` 可跳过分支检查) | +| 2 | npm 认证 | 检查 `npm whoami`,未登录则中止 | +| 3 | 更新版本号 | 同时更新 `packages/core` 和 `packages/cli` 的 version | +| 4 | 质量检查 | `npm run check`(typecheck + eslint + prettier) | +| 5 | 测试 | `npm run test --workspaces` | +| 6 | 构建 | `npm run build`(core tsc + cli esbuild bundle) | +| 7 | 发布 core | `npm publish --workspace=@vegamo/deepcode-core --access public` | +| 8 | 发布 cli | 将 cli 的 `@vegamo/deepcode-core` 依赖从 `file:../core` 临时改为 `^`,发布后恢复 | +| 9 | Git commit & tag | `chore(release): v` + `git tag v` | + +### 完整示例 + +```bash +# 发布正式版 +npm run prepare:package -- 0.1.32 + +# 发布 beta 版 +npm run prepare:package -- 0.1.32-beta.1 --tag beta + +# 预演(不实际发布,用于检查流程) +npm run prepare:package -- 0.1.32 --dry-run + +# 从非 main 分支发布 +npm run prepare:package -- 0.1.32 --force +``` + +### 关于 file:../core 依赖 + +CLI 包的 `@vegamo/deepcode-core` 依赖在开发时使用 `"file:../core"`(monorepo 本地链接)。发布到 npm 时,脚本会自动将其替换为 `"^"`,发布完成后恢复为 `file:../core`。这个过程对用户透明,无需手动处理。 + +### 发布后 + +脚本完成后会提示手动推送到 remote: + +```bash +git push && git push --tags +``` + +验证发布结果: + +```bash +npm view @vegamo/deepcode-cli version +npx @vegamo/deepcode-cli --version +``` + +--- + +## 典型发布流程 + +一个完整的版本发布通常按以下步骤进行: + +```bash +# 1. 确保工作区干净 +git status + +# 2. 升级版本号 +npm run release:version -- patch + +# 3. 检查变更 +git diff + +# 4. 提交版本变更 +git add -A +git commit -m "chore(release): v0.1.32" + +# 5. 构建 + 质量检查 + 发布 +npm run prepare:package -- 0.1.32 + +# 6. 推送到 remote +git push && git push --tags +``` + +也可以简化为两步(`prepare:package` 会自动 commit 和 tag): + +```bash +npm run release:version -- patch +npm run prepare:package -- 0.1.32 +git push && git push --tags +``` + +--- + +## 预发布版本流程 + +```bash +# 第一个 beta +npm run release:version -- prerelease --preid beta +# → 0.1.32-beta.0 + +git add -A && git commit -m "chore(release): v0.1.32-beta.0" +npm run prepare:package -- 0.1.32-beta.0 --tag beta + +# 后续 beta +npm run release:version -- prerelease --preid beta +# → 0.1.32-beta.1 + +git add -A && git commit -m "chore(release): v0.1.32-beta.1" +npm run prepare:package -- 0.1.32-beta.1 --tag beta + +# 正式发布 +npm run release:version -- patch +# → 0.1.32 + +git add -A && git commit -m "chore(release): v0.1.32" +npm run prepare:package -- 0.1.32 +git push && git push --tags +``` diff --git a/RELEASE_en.md b/RELEASE_en.md new file mode 100644 index 00000000..4844bf70 --- /dev/null +++ b/RELEASE_en.md @@ -0,0 +1,235 @@ +# Release + +Deep Code uses two scripts to manage version releases in the monorepo: + +| Script | Command | Purpose | +|--------|---------|---------| +| `scripts/version.js` | `npm run release:version` | Bump all workspace package versions + regenerate lockfile | +| `scripts/prepare-package.js` | `npm run prepare:package` | Build + quality checks + publish to npm + git commit & tag | + +Use them together: bump version first, then publish. + +--- + +## release:version — Version Bump + +Works like `npm version`, supporting all standard bump types. + +### Basic Usage + +```bash +npm run release:version -- [options] +``` + +> Note: npm scripts require the `--` separator to pass arguments. + +### Supported Bump Types + +| Type | Current | Result | Description | +|------|---------|--------|-------------| +| `patch` | `0.1.31` | `0.1.32` | Patch version +1 | +| `minor` | `0.1.31` | `0.2.0` | Minor version +1, patch reset | +| `major` | `0.1.31` | `1.0.0` | Major version +1, minor/patch reset | +| `prepatch` | `0.1.31` | `0.1.32-0` | Pre-release patch | +| `preminor` | `0.1.31` | `0.2.0-0` | Pre-release minor | +| `premajor` | `0.1.31` | `1.0.0-0` | Pre-release major | +| `prerelease` | `0.1.31` | `0.1.32-0` | Increment pre-release number | +| `from-git` | — | Read from latest git tag | For cases where tag exists but package.json not updated | + +You can also specify an exact version: + +```bash +npm run release:version -- 0.2.0 +``` + +### Pre-release Chain + +`prerelease` supports chained increments: + +``` +0.1.31 + → prerelease → 0.1.32-beta.0 + → prerelease → 0.1.32-beta.1 + → prerelease → 0.1.32-beta.2 + → patch → 0.1.32 (drops prerelease suffix) +``` + +### --preid Option + +Pre-release identifier, defaults to `"0"`, customizable: + +```bash +npm run release:version -- prerelease --preid beta +# 0.1.31 → 0.1.32-beta.0 + +npm run release:version -- premajor --preid alpha +# 0.1.31 → 1.0.0-alpha.0 +``` + +### What It Does + +1. Reads current version from `packages/core/package.json` +2. Calculates target version based on bump type +3. Updates `version` field in **all** `packages/*/package.json` (core, cli, vscode-ide-companion) +4. Deletes old `package-lock.json` and regenerates via `npm install --package-lock-only` + +### Examples + +```bash +# Bump patch +npm run release:version -- patch + +# Bump minor +npm run release:version -- minor + +# Beta pre-release +npm run release:version -- prerelease --preid beta + +# Exact version +npm run release:version -- 0.2.0 + +# From git tag +npm run release:version -- from-git +``` + +After bumping, review changes and commit: + +```bash +git diff +git add -A +git commit -m "chore(release): v0.1.32" +git tag v0.1.32 +``` + +--- + +## prepare:package — Build and Publish to npm + +Runs quality checks, builds, publishes both npm packages, and automatically creates a git commit with tag. + +### Basic Usage + +```bash +npm run prepare:package -- [options] +``` + +### Arguments + +| Argument | Description | +|----------|-------------| +| `` | **Required**. Semver version to publish | +| `--tag ` | npm dist-tag, default `"latest"`, commonly `beta` or `next` | +| `--dry-run` | Preview mode, no actual writes | +| `--force` | Skip main branch check, allow publishing from other branches | + +### Execution Flow (9 Steps) + +| Step | Action | Description | +|------|--------|-------------| +| 1 | Git check | Working tree must be clean, must be on main branch (`--force` skips branch check) | +| 2 | npm auth | Checks `npm whoami`, aborts if not logged in | +| 3 | Update versions | Updates `packages/core` and `packages/cli` version fields | +| 4 | Quality checks | `npm run check` (typecheck + eslint + prettier) | +| 5 | Tests | `npm run test --workspaces` | +| 6 | Build | `npm run build` (core tsc + cli esbuild bundle) | +| 7 | Publish core | `npm publish --workspace=@vegamo/deepcode-core --access public` | +| 8 | Publish cli | Temporarily changes cli's `@vegamo/deepcode-core` dep from `file:../core` to `^`, restores after publish | +| 9 | Git commit & tag | `chore(release): v` + `git tag v` | + +### Examples + +```bash +# Publish stable release +npm run prepare:package -- 0.1.32 + +# Publish beta +npm run prepare:package -- 0.1.32-beta.1 --tag beta + +# Dry run (no actual publish) +npm run prepare:package -- 0.1.32 --dry-run + +# Publish from non-main branch +npm run prepare:package -- 0.1.32 --force +``` + +### About the file:../core Dependency + +The CLI package uses `"file:../core"` for the `@vegamo/deepcode-core` dependency during development (monorepo local link). When publishing to npm, the script automatically replaces it with `"^"` and restores it after publishing. This is transparent — no manual handling required. + +### After Publishing + +The script prompts you to push to remote: + +```bash +git push && git push --tags +``` + +Verify the release: + +```bash +npm view @vegamo/deepcode-cli version +npx @vegamo/deepcode-cli --version +``` + +--- + +## Typical Release Flow + +A complete version release follows these steps: + +```bash +# 1. Ensure clean working tree +git status + +# 2. Bump version +npm run release:version -- patch + +# 3. Review changes +git diff + +# 4. Commit version change +git add -A +git commit -m "chore(release): v0.1.32" + +# 5. Build + quality check + publish +npm run prepare:package -- 0.1.32 + +# 6. Push to remote +git push && git push --tags +``` + +Or simplified to two steps (`prepare:package` auto-commits and tags): + +```bash +npm run release:version -- patch +npm run prepare:package -- 0.1.32 +git push && git push --tags +``` + +--- + +## Pre-release Flow + +```bash +# First beta +npm run release:version -- prerelease --preid beta +# → 0.1.32-beta.0 + +git add -A && git commit -m "chore(release): v0.1.32-beta.0" +npm run prepare:package -- 0.1.32-beta.0 --tag beta + +# Subsequent betas +npm run release:version -- prerelease --preid beta +# → 0.1.32-beta.1 + +git add -A && git commit -m "chore(release): v0.1.32-beta.1" +npm run prepare:package -- 0.1.32-beta.1 --tag beta + +# Stable release +npm run release:version -- patch +# → 0.1.32 + +git add -A && git commit -m "chore(release): v0.1.32" +npm run prepare:package -- 0.1.32 +git push && git push --tags +``` diff --git a/package.json b/package.json index e2a722c0..165ccfdb 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "start": "node scripts/start.js", "build-and-start": "npm run build && npm run start", "test": "npm run test --workspaces --if-present", - "prepare": "husky" + "release:version": "node scripts/version.js", + "prepare:package": "node scripts/prepare-package.js", + "prepare": "husky && npm run build && npm run bundle" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 5ad51fb7..fd4da3ac 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -31,6 +31,7 @@ "files": [ "out/extension.js", "resources/**", + "templates/**", "README.md", "README_cn.md", "README_en.md", diff --git a/scripts/build-vscode-companion.js b/scripts/build-vscode-companion.js index 7288c786..7fbb1c2a 100644 --- a/scripts/build-vscode-companion.js +++ b/scripts/build-vscode-companion.js @@ -1,4 +1,5 @@ import { spawnSync } from "node:child_process"; +import { cpSync, existsSync, rmSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -16,8 +17,22 @@ console.log("========================================="); console.log(" Deep Code — Build VSCode Companion"); console.log("========================================="); -run("npm", ["run", "build", "--workspace=@vegamo/deepcode-core"], "1/3 Build core"); -run("node", ["scripts/esbuild-vscode.config.js"], "2/3 Bundle extension"); -run("npm", ["run", "package", "--workspace=deepcode-vscode"], "3/3 Package .vsix"); +run("npm", ["run", "build", "--workspace=@vegamo/deepcode-core"], "1/4 Build core"); +run("node", ["scripts/esbuild-vscode.config.js"], "2/4 Bundle extension"); + +// Copy templates from core so the extension can read them at runtime via fs +const templatesSrc = join(root, "packages", "core", "templates"); +const templatesDest = join(root, "packages", "vscode-ide-companion", "templates"); + +if (!existsSync(templatesSrc)) { + console.error(`\n❌ Templates not found at ${templatesSrc}`); + process.exit(1); +} + +rmSync(templatesDest, { recursive: true, force: true }); +cpSync(templatesSrc, templatesDest, { recursive: true, dereference: true }); +console.log("\n[3/4] Copied templates from core → vscode-ide-companion/templates/"); + +run("npm", ["run", "package", "--workspace=deepcode-vscode"], "4/4 Package .vsix"); console.log("\n✅ VSCode companion build complete.\n\n"); diff --git a/scripts/prepare-package.js b/scripts/prepare-package.js new file mode 100644 index 00000000..6049f082 --- /dev/null +++ b/scripts/prepare-package.js @@ -0,0 +1,302 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function log(msg) { + console.log(msg); +} + +function step(n, total, msg) { + console.log(`\n[${n}/${total}] ${msg}`); +} + +function fail(msg) { + console.error(`\n❌ ${msg}`); + process.exit(1); +} + +function ok(msg) { + console.log(`✅ ${msg}`); +} + +function run(cmd, args, opts = {}) { + const label = opts.label ?? `${cmd} ${args.join(" ")}`; + if (opts.dryRun) { + log(` (dry-run) ${label}`); + return { status: 0, stdout: "" }; + } + const result = spawnSync(cmd, args, { + stdio: opts.stdio ?? "inherit", + cwd: opts.cwd ?? root, + shell: true, + env: { ...process.env, ...opts.env }, + }); + if (result.status !== 0) { + fail(`Command failed: ${label}`); + } + return result; +} + +function readJson(filePath) { + return JSON.parse(readFileSync(filePath, "utf-8")); +} + +function writeJson(filePath, data) { + writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8"); +} + +function isValidSemver(v) { + return /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(v); +} + +// ── Parse args ─────────────────────────────────────────────────────────────── + +const args = process.argv.slice(2); +let version = null; +let tag = "latest"; +let dryRun = false; +let force = false; + +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--dry-run") { + dryRun = true; + } else if (arg === "--force") { + force = true; + } else if (arg === "--tag") { + tag = args[++i]; + if (!tag) fail("--tag requires a value"); + } else if (!version) { + version = arg; + } else { + fail(`Unknown argument: ${arg}`); + } +} + +if (!version) { + log(` +Usage: node scripts/publish.js [options] + +Arguments: + Semver version to publish (e.g. 0.1.32, 0.2.0-beta.1) + +Options: + --tag npm dist-tag (default: "latest") + --dry-run Preview all steps without executing + --force Skip branch check (publish from non-main branch) + +Examples: + node scripts/publish.js 0.1.32 + node scripts/publish.js 0.1.32-beta.1 --tag beta + node scripts/publish.js 0.1.32 --dry-run +`); + process.exit(1); +} + +if (!isValidSemver(version)) { + fail(`Invalid semver version: ${version}`); +} + +const TOTAL_STEPS = 9; + +// ── Banner ─────────────────────────────────────────────────────────────────── + +log("========================================="); +log(` Deep Code CLI — Publish v${version}`); +log(` tag=${tag} dryRun=${dryRun} force=${force}`); +log("========================================="); + +// ── 1. Git checks ──────────────────────────────────────────────────────────── + +step(1, TOTAL_STEPS, "Checking git state..."); + +const gitStatus = spawnSync("git", ["status", "--porcelain"], { + cwd: root, + encoding: "utf-8", + shell: true, +}); +if (gitStatus.stdout.trim()) { + fail("Working tree is not clean. Commit or stash changes first."); +} +ok("Working tree is clean"); + +if (!force) { + const gitBranch = spawnSync("git", ["branch", "--show-current"], { + cwd: root, + encoding: "utf-8", + shell: true, + }); + const branch = gitBranch.stdout.trim(); + if (branch !== "main") { + fail(`Not on main branch (current: ${branch}). Use --force to publish from another branch.`); + } + ok("On main branch"); +} + +// ── 2. npm auth ────────────────────────────────────────────────────────────── + +step(2, TOTAL_STEPS, "Checking npm authentication..."); + +if (!dryRun) { + const whoami = spawnSync("npm", ["whoami"], { + cwd: root, + encoding: "utf-8", + shell: true, + }); + if (whoami.status !== 0) { + fail("Not logged in to npm. Run `npm login` first."); + } + ok(`Logged in as: ${whoami.stdout.trim()}`); +} else { + log(" (dry-run) skipping npm whoami"); +} + +// ── 3. Version bump ────────────────────────────────────────────────────────── + +step(3, TOTAL_STEPS, "Updating package versions..."); + +const corePkgPath = join(root, "packages", "core", "package.json"); +const cliPkgPath = join(root, "packages", "cli", "package.json"); + +const corePkg = readJson(corePkgPath); +const cliPkg = readJson(cliPkgPath); + +const oldVersion = corePkg.version; + +// Save originals for restore +const origCliPkg = JSON.stringify(cliPkg, null, 2) + "\n"; + +corePkg.version = version; +cliPkg.version = version; + +if (!dryRun) { + writeJson(corePkgPath, corePkg); + writeJson(cliPkgPath, cliPkg); + ok(`Updated packages/core: ${oldVersion} → ${version}`); + ok(`Updated packages/cli: ${oldVersion} → ${version}`); +} else { + log(` (dry-run) packages/core: ${oldVersion} → ${version}`); + log(` (dry-run) packages/cli: ${oldVersion} → ${version}`); +} + +// ── 4. Quality checks ──────────────────────────────────────────────────────── + +step(4, TOTAL_STEPS, "Running quality checks (typecheck + lint + format)..."); + +run("npm", ["run", "check"], { dryRun }); +ok("All checks passed"); + +step(5, TOTAL_STEPS, "Running tests..."); + +run("npm", ["run", "test", "--workspaces"], { dryRun }); +ok("All tests passed"); + +// ── 6. Build ───────────────────────────────────────────────────────────────── + +step(6, TOTAL_STEPS, "Building packages..."); + +run("npm", ["run", "build"], { dryRun }); +ok("Build complete"); + +// ── 7. Publish core ────────────────────────────────────────────────────────── + +step(7, TOTAL_STEPS, "Publishing @vegamo/deepcode-core..."); + +const corePublishArgs = [ + "publish", + "--workspace=@vegamo/deepcode-core", + "--access", + "public", + "--tag", + tag, + "--registry", + "https://registry.npmjs.org", +]; +if (dryRun) corePublishArgs.push("--dry-run"); + +run("npm", corePublishArgs, { dryRun, label: `npm ${corePublishArgs.join(" ")}` }); +ok(`Published @vegamo/deepcode-core@${version}`); + +// ── 8. Patch CLI deps & publish ────────────────────────────────────────────── + +step(8, TOTAL_STEPS, "Patching CLI dependencies and publishing @vegamo/deepcode-cli..."); + +// Replace file:../core with ^version for npm +const patchedCliPkg = readJson(cliPkgPath); +const coreDep = patchedCliPkg.dependencies["@vegamo/deepcode-core"]; +if (coreDep && coreDep.startsWith("file:")) { + patchedCliPkg.dependencies["@vegamo/deepcode-core"] = `^${version}`; + if (!dryRun) { + writeJson(cliPkgPath, patchedCliPkg); + } + log(` Patched @vegamo/deepcode-core dep: "${coreDep}" → "^${version}"`); +} + +const cliPublishArgs = [ + "publish", + "--workspace=@vegamo/deepcode-cli", + "--access", + "public", + "--tag", + tag, + "--registry", + "https://registry.npmjs.org", +]; +if (dryRun) cliPublishArgs.push("--dry-run"); + +run("npm", cliPublishArgs, { dryRun, label: `npm ${cliPublishArgs.join(" ")}` }); +ok(`Published @vegamo/deepcode-cli@${version}`); + +// Restore file:../core for local development +if (!dryRun) { + writeJson(cliPkgPath, JSON.parse(origCliPkg)); + // But keep the new version + const restoredCli = readJson(cliPkgPath); + restoredCli.version = version; + writeJson(cliPkgPath, restoredCli); + log(" Restored @vegamo/deepcode-core dep to file:../core"); +} + +// ── 9. Git commit + tag ────────────────────────────────────────────────────── + +step(9, TOTAL_STEPS, "Creating git commit and tag..."); + +if (!dryRun) { + run("git", ["add", "packages/core/package.json", "packages/cli/package.json"], { + label: "git add packages/*/package.json", + }); + run("git", ["commit", "-m", `chore(release): v${version}`], { + label: `git commit -m "chore(release): v${version}"`, + }); + run("git", ["tag", `v${version}`], { + label: `git tag v${version}`, + }); + ok(`Created commit and tag v${version}`); +} else { + log(` (dry-run) git add + commit "chore(release): v${version}"`); + log(` (dry-run) git tag v${version}`); +} + +// ── Done ───────────────────────────────────────────────────────────────────── + +console.log("\n========================================="); +console.log(` 🎉 Published v${version} successfully!`); +console.log("========================================="); +console.log(` + Packages published: + • @vegamo/deepcode-core@${version} + • @vegamo/deepcode-cli@${version} + + Verify: + npm view @vegamo/deepcode-cli version + npx @vegamo/deepcode-cli --version + + Push to remote: + git push && git push --tags +`); diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 00000000..5d2fa4f1 --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,296 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync, writeFileSync, unlinkSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { globSync } from "glob"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); + +const BUMP_TYPES = ["major", "minor", "patch", "premajor", "preminor", "prepatch", "prerelease", "from-git"]; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function log(msg) { + console.log(msg); +} + +function fail(msg) { + console.error(`\n❌ ${msg}`); + process.exit(1); +} + +function ok(msg) { + console.log(`✅ ${msg}`); +} + +function readJson(filePath) { + return JSON.parse(readFileSync(filePath, "utf-8")); +} + +function writeJson(filePath, data) { + writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8"); +} + +function isValidSemver(v) { + return /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(v); +} + +function isBumpType(v) { + return BUMP_TYPES.includes(v); +} + +function run(cmd, args, opts = {}) { + const result = spawnSync(cmd, args, { + stdio: opts.stdio ?? "inherit", + cwd: opts.cwd ?? root, + shell: true, + }); + if (result.status !== 0) { + fail(`Command failed: ${cmd} ${args.join(" ")}`); + } + return result; +} + +function runSilent(cmd, args) { + const result = spawnSync(cmd, args, { + cwd: root, + encoding: "utf-8", + shell: true, + }); + if (result.status !== 0) { + return null; + } + return result.stdout.trim(); +} + +// ── Version bump logic ─────────────────────────────────────────────────────── + +function parseVersion(v) { + const match = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/); + if (!match) fail(`Cannot parse version: ${v}`); + return { + major: Number(match[1]), + minor: Number(match[2]), + patch: Number(match[3]), + prerelease: match[4] ?? null, + }; +} + +function formatVersion({ major, minor, patch, prerelease }) { + let v = `${major}.${minor}.${patch}`; + if (prerelease) v += `-${prerelease}`; + return v; +} + +function bumpVersion(current, type, preid) { + const v = parseVersion(current); + + switch (type) { + case "major": + return formatVersion({ major: v.major + 1, minor: 0, patch: 0, prerelease: null }); + + case "minor": + return formatVersion({ major: v.major, minor: v.minor + 1, patch: 0, prerelease: null }); + + case "patch": + if (v.prerelease) { + // 0.1.32-beta.1 → 0.1.32 (drop prerelease) + return formatVersion({ ...v, prerelease: null }); + } + return formatVersion({ ...v, patch: v.patch + 1 }); + + case "premajor": + return formatVersion({ + major: v.major + 1, + minor: 0, + patch: 0, + prerelease: `${preid}.0`, + }); + + case "preminor": + return formatVersion({ + major: v.major, + minor: v.minor + 1, + patch: 0, + prerelease: `${preid}.0`, + }); + + case "prepatch": + if (v.prerelease) { + // Already a prerelease — increment the prerelease number + const num = Number(v.prerelease.split(".").pop()); + const base = v.prerelease.split(".").slice(0, -1).join("."); + if (!isNaN(num)) { + return formatVersion({ ...v, prerelease: `${base}.${num + 1}` }); + } + } + return formatVersion({ + ...v, + patch: v.patch + 1, + prerelease: `${preid}.0`, + }); + + case "prerelease": + if (v.prerelease) { + // 0.1.32-beta.0 → 0.1.32-beta.1 + const num = Number(v.prerelease.split(".").pop()); + const base = v.prerelease.split(".").slice(0, -1).join("."); + if (!isNaN(num)) { + const newPre = base ? `${base}.${num + 1}` : `${num + 1}`; + return formatVersion({ ...v, prerelease: newPre }); + } + // Can't parse number, append .0 + return formatVersion({ ...v, prerelease: `${v.prerelease}.0` }); + } + // No prerelease yet — go to next patch prerelease + return formatVersion({ + ...v, + patch: v.patch + 1, + prerelease: `${preid}.0`, + }); + + default: + fail(`Unknown bump type: ${type}`); + } +} + +function resolveVersionFromGit() { + // Get latest tag matching v* + const tag = runSilent("git", ["describe", "--tags", "--abbrev=0"]); + if (!tag) { + fail("No git tags found. Cannot use 'from-git'."); + } + const v = tag.replace(/^v/, ""); + if (!isValidSemver(v)) { + fail(`Latest git tag is not a valid semver: ${tag}`); + } + return v; +} + +// ── Parse args ─────────────────────────────────────────────────────────────── + +const args = process.argv.slice(2); +let bumpArg = null; +let preid = "0"; + +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--help" || arg === "-h") { + log(` +Usage: npm run release:version -- [--preid ] + +Bumps all workspace package.json files and regenerates package-lock.json. +Works like npm version but for the entire monorepo. + +Bump types: + major 0.1.31 → 1.0.0 + minor 0.1.31 → 0.2.0 + patch 0.1.31 → 0.1.32 + premajor 0.1.31 → 1.0.0-0 + preminor 0.1.31 → 0.2.0-0 + prepatch 0.1.31 → 0.1.32-0 + prerelease 0.1.31 → 0.1.32-0 0.1.32-0 → 0.1.32-1 + from-git Use version from latest git tag + +Options: + --preid Prerelease identifier (default: "0", e.g. "beta", "alpha") + +Examples: + npm run release:version -- patch + npm run release:version -- minor + npm run release:version -- 0.2.0 + npm run release:version -- prerelease --preid beta + npm run release:version -- from-git +`); + process.exit(0); + } else if (arg === "--preid") { + preid = args[++i]; + if (!preid) fail("--preid requires a value"); + } else if (!bumpArg) { + bumpArg = arg; + } else { + fail(`Unknown argument: ${arg}`); + } +} + +if (!bumpArg) { + log(` +Usage: npm run release:version -- [--preid ] + Run with --help for details. +`); + process.exit(1); +} + +// ── Resolve target version ─────────────────────────────────────────────────── + +const corePkgPath = join(root, "packages", "core", "package.json"); +const currentVersion = readJson(corePkgPath).version; + +let version; + +if (bumpArg === "from-git") { + version = resolveVersionFromGit(); + log(`Resolved from git tag: v${version}`); +} else if (isBumpType(bumpArg)) { + version = bumpVersion(currentVersion, bumpArg, preid); +} else if (isValidSemver(bumpArg)) { + version = bumpArg; +} else { + fail(`Invalid argument: "${bumpArg}". Expected a bump type (${BUMP_TYPES.join(", ")}) or a semver version.`); +} + +// ── Banner ─────────────────────────────────────────────────────────────────── + +log("========================================="); +log(` Deep Code — Bump Version`); +log(` ${currentVersion} → ${version}`); +log("=========================================\n"); + +// ── Find all workspace package.json ────────────────────────────────────────── + +const pkgPaths = globSync("packages/*/package.json", { cwd: root, absolute: true }); + +if (pkgPaths.length === 0) { + fail("No workspace packages found under packages/"); +} + +// ── Update versions ────────────────────────────────────────────────────────── + +log("Updating package.json files:\n"); + +for (const pkgPath of pkgPaths) { + const pkg = readJson(pkgPath); + const oldVersion = pkg.version; + pkg.version = version; + writeJson(pkgPath, pkg); + const short = pkgPath.replace(root + "/", ""); + log(` ${short}: ${oldVersion} → ${version}`); +} + +// ── Regenerate lockfile ────────────────────────────────────────────────────── + +log("\nRegenerating package-lock.json...\n"); + +const lockPath = join(root, "package-lock.json"); +try { + unlinkSync(lockPath); + log(" Removed old package-lock.json"); +} catch { + // lockfile may not exist, that's fine +} + +run("npm", ["install", "--package-lock-only"]); +ok("package-lock.json regenerated"); + +// ── Done ───────────────────────────────────────────────────────────────────── + +console.log("\n========================================="); +log(` 🎉 Version bumped to v${version}`); +console.log("========================================="); +console.log(` + Updated ${pkgPaths.length} packages. Next steps: + git add -A && git commit -m "chore(release): v${version}" + git tag v${version} + git push && git push --tags +`); From 8fc6d696fdae44387cab561df18d331018783202 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 18 Jun 2026 11:22:26 +0800 Subject: [PATCH 176/212] =?UTF-8?q?refactor(scripts):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=B8=85=E7=90=86=E8=84=9A=E6=9C=AC=E4=BB=A5=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=E6=9E=84=E5=BB=BA=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除根目录下的 node_modules 文件夹 - 增加按包删除每个包的 node_modules、dist、生成目录和 tsbuildinfo 文件 - 清理 vscode-ide-companion 包的 out 和 templates 目录 - 修正 vscode-ide-companion 的 vsix 文件清理路径 - 输出的日志信息更明确,显示具体删除路径 --- package-lock.json | 1515 ++++++++++++++++++++++----------------------- scripts/clean.js | 38 +- 2 files changed, 778 insertions(+), 775 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8bd4ca15..e36f1926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.3.0", - "resolved": "https://registry.npmmirror.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", "integrity": "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==", "license": "MIT", "dependencies": { @@ -42,7 +42,7 @@ }, "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { @@ -54,14 +54,14 @@ }, "node_modules/@azu/format-text": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/@azu/format-text/-/format-text-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@azu/style-format": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@azu/style-format/-/style-format-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", "dev": true, "license": "WTFPL", @@ -71,7 +71,7 @@ }, "node_modules/@azure/abort-controller": { "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, "license": "MIT", @@ -84,7 +84,7 @@ }, "node_modules/@azure/core-auth": { "version": "1.10.1", - "resolved": "https://registry.npmmirror.com/@azure/core-auth/-/core-auth-1.10.1.tgz", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "dev": true, "license": "MIT", @@ -99,7 +99,7 @@ }, "node_modules/@azure/core-client": { "version": "1.10.2", - "resolved": "https://registry.npmmirror.com/@azure/core-client/-/core-client-1.10.2.tgz", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.2.tgz", "integrity": "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==", "dev": true, "license": "MIT", @@ -118,7 +118,7 @@ }, "node_modules/@azure/core-rest-pipeline": { "version": "1.24.0", - "resolved": "https://registry.npmmirror.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.24.0.tgz", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.24.0.tgz", "integrity": "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==", "dev": true, "license": "MIT", @@ -137,7 +137,7 @@ }, "node_modules/@azure/core-tracing": { "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "dev": true, "license": "MIT", @@ -150,7 +150,7 @@ }, "node_modules/@azure/core-util": { "version": "1.13.1", - "resolved": "https://registry.npmmirror.com/@azure/core-util/-/core-util-1.13.1.tgz", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "dev": true, "license": "MIT", @@ -165,7 +165,7 @@ }, "node_modules/@azure/identity": { "version": "4.13.1", - "resolved": "https://registry.npmmirror.com/@azure/identity/-/identity-4.13.1.tgz", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", "dev": true, "license": "MIT", @@ -188,7 +188,7 @@ }, "node_modules/@azure/logger": { "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/@azure/logger/-/logger-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dev": true, "license": "MIT", @@ -201,22 +201,22 @@ } }, "node_modules/@azure/msal-browser": { - "version": "5.13.0", - "resolved": "https://registry.npmmirror.com/@azure/msal-browser/-/msal-browser-5.13.0.tgz", - "integrity": "sha512-Ea23x0U8XNFY+qJ9T44zO2BbY+AHdb+WdjmYnx36OhJ/KO+PGU5pmsNHf1DCElYX+6wyVRJz1HFeCPC/cHbRug==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.14.0.tgz", + "integrity": "sha512-Dfl7hPZe9/JJwRhFFXHq2z1oHYBuGubmff3kWXOsd1AGgyXlqjNYAWuN/1JL/ZrcZBs8TKMjGSil6Rcc7E8VPQ==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "16.8.0" + "@azure/msal-common": "16.9.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "16.8.0", - "resolved": "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-16.8.0.tgz", - "integrity": "sha512-5S4RHOcInL2Nu2U217tDZbWGI6StMfcWCrA7TWvWdJmXQ+cYrrIqr84AsN62fGh2MDBysiBJPt6CfWceJfloEA==", + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.9.0.tgz", + "integrity": "sha512-1MWGjqgUCRAYgLmVFZKp7fs3Rg1TFvIMgywY8ze2olNVvLlJoRThuoziWSDJuwwyJI5L4rnLb9Tyt5D9GvSLPw==", "dev": true, "license": "MIT", "engines": { @@ -224,13 +224,13 @@ } }, "node_modules/@azure/msal-node": { - "version": "5.2.4", - "resolved": "https://registry.npmmirror.com/@azure/msal-node/-/msal-node-5.2.4.tgz", - "integrity": "sha512-rpBUg9dA8UpC2WiFt3KeDKVQmmmVrfxdRnW+F1ebgou/jX/0tAvYuonaq5RUo8OaqzOrj4x/HaI8DmY56RXZ2Q==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.5.tgz", + "integrity": "sha512-RUuewWk9JvWJS5Yiy8/74Lm1rQAWlrU/qg/Bgtk1jIauVRtnb9XKwS5Xg0J+Whwjesq9EVrBIFgQEP8vHxgezA==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "16.8.0", + "@azure/msal-common": "16.9.0", "jsonwebtoken": "^9.0.0" }, "engines": { @@ -239,7 +239,7 @@ }, "node_modules/@babel/code-frame": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", @@ -254,7 +254,7 @@ }, "node_modules/@babel/compat-data": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", @@ -264,7 +264,7 @@ }, "node_modules/@babel/core": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", @@ -295,7 +295,7 @@ }, "node_modules/@babel/generator": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", @@ -312,7 +312,7 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", @@ -329,7 +329,7 @@ }, "node_modules/@babel/helper-globals": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", @@ -339,7 +339,7 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", @@ -353,7 +353,7 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", @@ -371,7 +371,7 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", @@ -381,7 +381,7 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", @@ -391,7 +391,7 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", @@ -401,7 +401,7 @@ }, "node_modules/@babel/helpers": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", @@ -415,7 +415,7 @@ }, "node_modules/@babel/parser": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", @@ -431,7 +431,7 @@ }, "node_modules/@babel/template": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", @@ -446,7 +446,7 @@ }, "node_modules/@babel/traverse": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", @@ -465,7 +465,7 @@ }, "node_modules/@babel/types": { "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", @@ -479,7 +479,7 @@ }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" @@ -496,7 +496,7 @@ }, "node_modules/@esbuild/android-arm": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" @@ -513,7 +513,7 @@ }, "node_modules/@esbuild/android-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" @@ -530,7 +530,7 @@ }, "node_modules/@esbuild/android-x64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" @@ -547,7 +547,7 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" @@ -564,7 +564,7 @@ }, "node_modules/@esbuild/darwin-x64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" @@ -581,7 +581,7 @@ }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" @@ -598,7 +598,7 @@ }, "node_modules/@esbuild/freebsd-x64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" @@ -615,7 +615,7 @@ }, "node_modules/@esbuild/linux-arm": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" @@ -632,7 +632,7 @@ }, "node_modules/@esbuild/linux-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" @@ -649,7 +649,7 @@ }, "node_modules/@esbuild/linux-ia32": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" @@ -666,7 +666,7 @@ }, "node_modules/@esbuild/linux-loong64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" @@ -683,7 +683,7 @@ }, "node_modules/@esbuild/linux-mips64el": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" @@ -700,7 +700,7 @@ }, "node_modules/@esbuild/linux-ppc64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" @@ -717,7 +717,7 @@ }, "node_modules/@esbuild/linux-riscv64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" @@ -734,7 +734,7 @@ }, "node_modules/@esbuild/linux-s390x": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" @@ -751,7 +751,7 @@ }, "node_modules/@esbuild/linux-x64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" @@ -768,7 +768,7 @@ }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" @@ -785,7 +785,7 @@ }, "node_modules/@esbuild/netbsd-x64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" @@ -802,7 +802,7 @@ }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" @@ -819,7 +819,7 @@ }, "node_modules/@esbuild/openbsd-x64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" @@ -836,7 +836,7 @@ }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" @@ -853,7 +853,7 @@ }, "node_modules/@esbuild/sunos-x64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" @@ -870,7 +870,7 @@ }, "node_modules/@esbuild/win32-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" @@ -887,7 +887,7 @@ }, "node_modules/@esbuild/win32-ia32": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" @@ -904,7 +904,7 @@ }, "node_modules/@esbuild/win32-x64": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" @@ -921,7 +921,7 @@ }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", @@ -940,7 +940,7 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", @@ -953,7 +953,7 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", @@ -963,7 +963,7 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.2", - "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.2.tgz", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", @@ -978,7 +978,7 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", @@ -991,7 +991,7 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", @@ -1004,7 +1004,7 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.5", - "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", @@ -1028,7 +1028,7 @@ }, "node_modules/@eslint/js": { "version": "9.39.4", - "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.4.tgz", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", @@ -1041,7 +1041,7 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", @@ -1051,7 +1051,7 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", @@ -1065,7 +1065,7 @@ }, "node_modules/@humanfs/core": { "version": "0.19.2", - "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", @@ -1078,7 +1078,7 @@ }, "node_modules/@humanfs/node": { "version": "0.16.8", - "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", @@ -1093,7 +1093,7 @@ }, "node_modules/@humanfs/types": { "version": "0.15.0", - "resolved": "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", @@ -1103,7 +1103,7 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", @@ -1117,7 +1117,7 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", @@ -1131,7 +1131,7 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", @@ -1142,7 +1142,7 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", @@ -1153,7 +1153,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", @@ -1163,14 +1163,14 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", @@ -1181,7 +1181,7 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", @@ -1195,7 +1195,7 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", @@ -1205,7 +1205,7 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", @@ -1219,7 +1219,7 @@ }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", "dev": true, "license": "MIT", @@ -1232,7 +1232,7 @@ }, "node_modules/@secretlint/config-loader": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", "dev": true, "license": "MIT", @@ -1250,7 +1250,7 @@ }, "node_modules/@secretlint/config-loader/node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", @@ -1267,14 +1267,14 @@ }, "node_modules/@secretlint/config-loader/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/core": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/core/-/core-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", "dev": true, "license": "MIT", @@ -1290,7 +1290,7 @@ }, "node_modules/@secretlint/formatter": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/formatter/-/formatter-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "license": "MIT", @@ -1313,7 +1313,7 @@ }, "node_modules/@secretlint/formatter/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", @@ -1326,7 +1326,7 @@ }, "node_modules/@secretlint/node": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/node/-/node-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", "dev": true, "license": "MIT", @@ -1346,21 +1346,21 @@ }, "node_modules/@secretlint/profiler": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/profiler/-/profiler-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/resolver": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/resolver/-/resolver-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/secretlint-formatter-sarif": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", "dev": true, "license": "MIT", @@ -1370,7 +1370,7 @@ }, "node_modules/@secretlint/secretlint-rule-no-dotenv": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", "dev": true, "license": "MIT", @@ -1383,7 +1383,7 @@ }, "node_modules/@secretlint/secretlint-rule-preset-recommend": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", "dev": true, "license": "MIT", @@ -1393,7 +1393,7 @@ }, "node_modules/@secretlint/source-creator": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", "dev": true, "license": "MIT", @@ -1407,7 +1407,7 @@ }, "node_modules/@secretlint/types": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/@secretlint/types/-/types-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", "dev": true, "license": "MIT", @@ -1417,7 +1417,7 @@ }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, "license": "MIT", @@ -1430,14 +1430,14 @@ }, "node_modules/@textlint/ast-node-types": { "version": "15.7.1", - "resolved": "https://registry.npmmirror.com/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.7.1.tgz", "integrity": "sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { "version": "15.7.1", - "resolved": "https://registry.npmmirror.com/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.7.1.tgz", "integrity": "sha512-TdwZ/debWYFD05K3CcoHtwvnCrza29wZxD+BjDTk/V5N7iRqkK1dTTHSD4A8AIgROLiDkHJmIKQbasbmsg8AvA==", "dev": true, "license": "MIT", @@ -1460,7 +1460,7 @@ }, "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", @@ -1470,14 +1470,14 @@ }, "node_modules/@textlint/linter-formatter/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", @@ -1487,14 +1487,14 @@ }, "node_modules/@textlint/linter-formatter/node_modules/pluralize": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/pluralize/-/pluralize-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", @@ -1509,7 +1509,7 @@ }, "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", @@ -1522,21 +1522,21 @@ }, "node_modules/@textlint/module-interop": { "version": "15.7.1", - "resolved": "https://registry.npmmirror.com/@textlint/module-interop/-/module-interop-15.7.1.tgz", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.7.1.tgz", "integrity": "sha512-Jg+sQW2L/cRJypk59wtcMUVVpt8vmit5ZMT3gUnFwevP3A6Qp1HfOtUy9ObT4hBX3lOSGT/ekcCDxR1pL7uH1g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { "version": "15.7.1", - "resolved": "https://registry.npmmirror.com/@textlint/resolver/-/resolver-15.7.1.tgz", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.7.1.tgz", "integrity": "sha512-8XnO0pgF6mXnm41VvWmBbEIdGPhiCUt31uLZkOis1ECeg/1SoUcIT6Mx/F0e1rukq8l0UlOSeY9a31CsvRMK0g==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { "version": "15.7.1", - "resolved": "https://registry.npmmirror.com/@textlint/types/-/types-15.7.1.tgz", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.7.1.tgz", "integrity": "sha512-Vye/GmFNBTgVzZFtIFJTmLB+s2A7oIADxNG6r9UhfPuY+Czv0z5G3xeyFZZudPlfxURsKUyPIU5XsjOFqVp33A==", "dev": true, "license": "MIT", @@ -1546,21 +1546,21 @@ }, "node_modules/@types/ejs": { "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/@types/ejs/-/ejs-3.1.5.tgz", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, "node_modules/@types/gradient-string": { "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/@types/gradient-string/-/gradient-string-1.1.6.tgz", + "resolved": "https://registry.npmjs.org/@types/gradient-string/-/gradient-string-1.1.6.tgz", "integrity": "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==", "license": "MIT", "dependencies": { @@ -1569,21 +1569,21 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/linkify-it": { "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "dev": true, "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", - "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, "license": "MIT", @@ -1594,14 +1594,14 @@ }, "node_modules/@types/mdurl": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "25.9.3", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.9.3.tgz", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "dev": true, "license": "MIT", @@ -1611,14 +1611,14 @@ }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", - "resolved": "https://registry.npmmirror.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "19.2.17", - "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.17.tgz", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", @@ -1628,36 +1628,36 @@ }, "node_modules/@types/sarif": { "version": "2.1.7", - "resolved": "https://registry.npmmirror.com/@types/sarif/-/sarif-2.1.7.tgz", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", "dev": true, "license": "MIT" }, "node_modules/@types/tinycolor2": { "version": "1.4.6", - "resolved": "https://registry.npmmirror.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", "license": "MIT" }, "node_modules/@types/vscode": { - "version": "1.120.0", - "resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.120.0.tgz", - "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", - "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/type-utils": "8.61.0", - "@typescript-eslint/utils": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1670,14 +1670,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.61.0", + "@typescript-eslint/parser": "^8.61.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", @@ -1686,16 +1686,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.61.0.tgz", - "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3" }, "engines": { @@ -1711,14 +1711,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", - "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.61.0", - "@typescript-eslint/types": "^8.61.0", + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", "debug": "^4.4.3" }, "engines": { @@ -1733,14 +1733,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", - "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0" + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1751,9 +1751,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", - "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", "dev": true, "license": "MIT", "engines": { @@ -1768,15 +1768,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", - "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1793,9 +1793,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.61.0.tgz", - "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", "dev": true, "license": "MIT", "engines": { @@ -1807,16 +1807,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", - "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.61.0", - "@typescript-eslint/tsconfig-utils": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1836,7 +1836,7 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", @@ -1846,7 +1846,7 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", @@ -1859,7 +1859,7 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", @@ -1875,7 +1875,7 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.8.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", @@ -1887,16 +1887,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.61.0.tgz", - "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0" + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1911,13 +1911,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", - "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/types": "8.61.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1930,7 +1930,7 @@ }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", @@ -1943,7 +1943,7 @@ }, "node_modules/@typespec/ts-http-runtime": { "version": "0.3.6", - "resolved": "https://registry.npmmirror.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.6.tgz", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.6.tgz", "integrity": "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==", "dev": true, "license": "MIT", @@ -1966,7 +1966,7 @@ }, "node_modules/@vscode/vsce": { "version": "3.9.2", - "resolved": "https://registry.npmmirror.com/@vscode/vsce/-/vsce-3.9.2.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", "integrity": "sha512-XSxMosEEDO6vLxELAHVkwmhC0qe0ijZni2jB9Rcs8kQsW4lhTDQ/wMzmwFs/buotAWSnpmUp/dRWD2ufG3UYKA==", "dev": true, "license": "MIT", @@ -2013,7 +2013,7 @@ }, "node_modules/@vscode/vsce-sign": { "version": "2.0.9", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", "dev": true, "hasInstallScript": true, @@ -2032,7 +2032,7 @@ }, "node_modules/@vscode/vsce-sign-alpine-arm64": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", "cpu": [ "arm64" @@ -2046,7 +2046,7 @@ }, "node_modules/@vscode/vsce-sign-alpine-x64": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", "cpu": [ "x64" @@ -2060,7 +2060,7 @@ }, "node_modules/@vscode/vsce-sign-darwin-arm64": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", "cpu": [ "arm64" @@ -2074,7 +2074,7 @@ }, "node_modules/@vscode/vsce-sign-darwin-x64": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", "cpu": [ "x64" @@ -2088,7 +2088,7 @@ }, "node_modules/@vscode/vsce-sign-linux-arm": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", "cpu": [ "arm" @@ -2102,7 +2102,7 @@ }, "node_modules/@vscode/vsce-sign-linux-arm64": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", "cpu": [ "arm64" @@ -2116,7 +2116,7 @@ }, "node_modules/@vscode/vsce-sign-linux-x64": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", "cpu": [ "x64" @@ -2130,7 +2130,7 @@ }, "node_modules/@vscode/vsce-sign-win32-arm64": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", "cpu": [ "arm64" @@ -2144,7 +2144,7 @@ }, "node_modules/@vscode/vsce-sign-win32-x64": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", "cpu": [ "x64" @@ -2158,7 +2158,7 @@ }, "node_modules/@vscode/vsce/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", @@ -2168,7 +2168,7 @@ }, "node_modules/@vscode/vsce/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", @@ -2181,7 +2181,7 @@ }, "node_modules/@vscode/vsce/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", @@ -2197,7 +2197,7 @@ }, "node_modules/@vscode/vsce/node_modules/semver": { "version": "7.8.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", @@ -2210,7 +2210,7 @@ }, "node_modules/acorn": { "version": "8.17.0", - "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", @@ -2223,7 +2223,7 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", @@ -2233,7 +2233,7 @@ }, "node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", @@ -2243,7 +2243,7 @@ }, "node_modules/ajv": { "version": "6.15.0", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", @@ -2260,7 +2260,7 @@ }, "node_modules/ansi-escapes": { "version": "7.3.0", - "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "license": "MIT", "dependencies": { @@ -2275,7 +2275,7 @@ }, "node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { @@ -2287,7 +2287,7 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", @@ -2303,13 +2303,13 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/astral-regex": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", @@ -2319,14 +2319,14 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/auto-bind": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/auto-bind/-/auto-bind-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", "license": "MIT", "engines": { @@ -2338,7 +2338,7 @@ }, "node_modules/azure-devops-node-api": { "version": "12.5.0", - "resolved": "https://registry.npmmirror.com/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "license": "MIT", @@ -2349,14 +2349,14 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ @@ -2377,9 +2377,9 @@ "optional": true }, "node_modules/baseline-browser-mapping": { - "version": "2.10.37", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", - "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2391,7 +2391,7 @@ }, "node_modules/binaryextensions": { "version": "6.11.0", - "resolved": "https://registry.npmmirror.com/binaryextensions/-/binaryextensions-6.11.0.tgz", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", "dev": true, "license": "Artistic-2.0", @@ -2407,7 +2407,7 @@ }, "node_modules/bl": { "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", @@ -2420,21 +2420,21 @@ }, "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, "license": "ISC" }, "node_modules/boundary": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/boundary/-/boundary-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/brace-expansion": { "version": "1.1.15", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", @@ -2445,7 +2445,7 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", @@ -2458,7 +2458,7 @@ }, "node_modules/browserslist": { "version": "4.28.2", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ @@ -2492,7 +2492,7 @@ }, "node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ @@ -2518,7 +2518,7 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", @@ -2528,14 +2528,14 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/bundle-name": { "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", @@ -2551,7 +2551,7 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", @@ -2565,7 +2565,7 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", @@ -2582,7 +2582,7 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", @@ -2592,7 +2592,7 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001799", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ @@ -2613,7 +2613,7 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", @@ -2630,7 +2630,7 @@ }, "node_modules/cheerio": { "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "dev": true, "license": "MIT", @@ -2656,7 +2656,7 @@ }, "node_modules/cheerio-select": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/cheerio-select/-/cheerio-select-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "license": "BSD-2-Clause", @@ -2674,7 +2674,7 @@ }, "node_modules/chownr": { "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "license": "ISC", @@ -2682,7 +2682,7 @@ }, "node_modules/cli-boxes": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-4.0.1.tgz", "integrity": "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==", "license": "MIT", "engines": { @@ -2693,33 +2693,31 @@ } }, "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" + "restore-cursor": "^4.0.0" }, "engines": { - "node": ">=18" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", - "dev": true, + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-6.0.0.tgz", + "integrity": "sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==", "license": "MIT", "dependencies": { - "slice-ansi": "^8.0.0", + "slice-ansi": "^9.0.0", "string-width": "^8.2.0" }, "engines": { - "node": ">=20" + "node": ">=22" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2727,7 +2725,7 @@ }, "node_modules/cockatiel": { "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/cockatiel/-/cockatiel-3.2.1.tgz", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", "dev": true, "license": "MIT", @@ -2737,7 +2735,7 @@ }, "node_modules/code-excerpt": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/code-excerpt/-/code-excerpt-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", "license": "MIT", "dependencies": { @@ -2749,7 +2747,7 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", @@ -2762,14 +2760,14 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", @@ -2782,7 +2780,7 @@ }, "node_modules/commander": { "version": "12.1.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", @@ -2792,21 +2790,21 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/convert-to-spaces": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", "license": "MIT", "engines": { @@ -2815,7 +2813,7 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", @@ -2830,7 +2828,7 @@ }, "node_modules/css-select": { "version": "5.2.2", - "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, "license": "BSD-2-Clause", @@ -2847,7 +2845,7 @@ }, "node_modules/css-what": { "version": "6.2.2", - "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, "license": "BSD-2-Clause", @@ -2860,14 +2858,14 @@ }, "node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", @@ -2885,7 +2883,7 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", @@ -2902,7 +2900,7 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", @@ -2913,7 +2911,7 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" @@ -2924,7 +2922,7 @@ }, "node_modules/default-browser": { "version": "5.5.0", - "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", @@ -2941,7 +2939,7 @@ }, "node_modules/default-browser-id": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", @@ -2954,7 +2952,7 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", @@ -2967,7 +2965,7 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", @@ -2977,7 +2975,7 @@ }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", @@ -2988,7 +2986,7 @@ }, "node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", @@ -3003,7 +3001,7 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ @@ -3016,7 +3014,7 @@ }, "node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", @@ -3032,7 +3030,7 @@ }, "node_modules/domutils": { "version": "3.2.2", - "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", @@ -3047,7 +3045,7 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", @@ -3062,7 +3060,7 @@ }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "license": "Apache-2.0", @@ -3072,7 +3070,7 @@ }, "node_modules/editions": { "version": "6.22.0", - "resolved": "https://registry.npmmirror.com/editions/-/editions-6.22.0.tgz", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", "dev": true, "license": "Artistic-2.0", @@ -3089,7 +3087,7 @@ }, "node_modules/ejs": { "version": "5.0.2", - "resolved": "https://registry.npmmirror.com/ejs/-/ejs-5.0.2.tgz", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz", "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==", "license": "Apache-2.0", "bin": { @@ -3100,22 +3098,22 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.372", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", - "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "version": "1.5.375", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", + "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==", "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { "version": "10.6.0", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, "node_modules/encoding-sniffer": { "version": "0.2.1", - "resolved": "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, "license": "MIT", @@ -3129,7 +3127,7 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", - "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", @@ -3140,7 +3138,7 @@ }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { @@ -3152,7 +3150,7 @@ }, "node_modules/environment": { "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "license": "MIT", "engines": { @@ -3164,7 +3162,7 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", @@ -3174,7 +3172,7 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", @@ -3184,7 +3182,7 @@ }, "node_modules/es-object-atoms": { "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", @@ -3197,7 +3195,7 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", @@ -3213,7 +3211,7 @@ }, "node_modules/es-toolkit": { "version": "1.47.1", - "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.47.1.tgz", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz", "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==", "license": "MIT", "workspaces": [ @@ -3223,7 +3221,7 @@ }, "node_modules/esbuild": { "version": "0.28.1", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.1.tgz", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, @@ -3265,7 +3263,7 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", @@ -3275,7 +3273,7 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", @@ -3288,7 +3286,7 @@ }, "node_modules/eslint": { "version": "9.39.4", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.4.tgz", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", @@ -3348,7 +3346,7 @@ }, "node_modules/eslint-config-prettier": { "version": "10.1.8", - "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", @@ -3364,7 +3362,7 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", @@ -3384,7 +3382,7 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", @@ -3401,7 +3399,7 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", @@ -3414,7 +3412,7 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", @@ -3432,7 +3430,7 @@ }, "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "license": "BSD-2-Clause", "bin": { @@ -3445,7 +3443,7 @@ }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", @@ -3458,7 +3456,7 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", @@ -3471,7 +3469,7 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", @@ -3481,7 +3479,7 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", @@ -3491,14 +3489,14 @@ }, "node_modules/eventemitter3": { "version": "5.0.4", - "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, "node_modules/expand-template": { "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, "license": "(MIT OR WTFPL)", @@ -3509,7 +3507,7 @@ }, "node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "license": "MIT", "dependencies": { @@ -3521,14 +3519,14 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", @@ -3545,7 +3543,7 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", @@ -3558,21 +3556,21 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ @@ -3589,7 +3587,7 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", @@ -3599,7 +3597,7 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", @@ -3617,7 +3615,7 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", @@ -3630,7 +3628,7 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", @@ -3643,7 +3641,7 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", @@ -3660,7 +3658,7 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", @@ -3674,14 +3672,14 @@ }, "node_modules/flatted": { "version": "3.4.2", - "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/form-data": { "version": "4.0.6", - "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", @@ -3698,7 +3696,7 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, "license": "MIT", @@ -3706,7 +3704,7 @@ }, "node_modules/fs-extra": { "version": "11.3.5", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.5.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, "license": "MIT", @@ -3721,7 +3719,7 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, @@ -3736,7 +3734,7 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", @@ -3746,7 +3744,7 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", @@ -3756,7 +3754,7 @@ }, "node_modules/get-east-asian-width": { "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", "engines": { @@ -3768,7 +3766,7 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", @@ -3793,7 +3791,7 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", @@ -3807,7 +3805,7 @@ }, "node_modules/github-from-package": { "version": "0.0.0", - "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "license": "MIT", @@ -3815,7 +3813,7 @@ }, "node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", @@ -3833,7 +3831,7 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", @@ -3846,7 +3844,7 @@ }, "node_modules/glob/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", @@ -3856,7 +3854,7 @@ }, "node_modules/glob/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", @@ -3869,7 +3867,7 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", @@ -3885,7 +3883,7 @@ }, "node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", @@ -3898,7 +3896,7 @@ }, "node_modules/globby": { "version": "14.1.0", - "resolved": "https://registry.npmmirror.com/globby/-/globby-14.1.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", "dev": true, "license": "MIT", @@ -3919,7 +3917,7 @@ }, "node_modules/globby/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", @@ -3929,7 +3927,7 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", @@ -3942,14 +3940,14 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/gradient-string": { "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/gradient-string/-/gradient-string-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-3.0.0.tgz", "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", "license": "MIT", "dependencies": { @@ -3962,7 +3960,7 @@ }, "node_modules/gradient-string/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { @@ -3974,7 +3972,7 @@ }, "node_modules/gray-matter": { "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", "license": "MIT", "dependencies": { @@ -3989,7 +3987,7 @@ }, "node_modules/gray-matter/node_modules/argparse": { "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "license": "MIT", "dependencies": { @@ -3998,7 +3996,7 @@ }, "node_modules/gray-matter/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { @@ -4011,7 +4009,7 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", @@ -4021,7 +4019,7 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", @@ -4034,7 +4032,7 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", @@ -4050,7 +4048,7 @@ }, "node_modules/hasown": { "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", @@ -4063,14 +4061,14 @@ }, "node_modules/hermes-estree": { "version": "0.25.1", - "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, "license": "MIT" }, "node_modules/hermes-parser": { "version": "0.25.1", - "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", @@ -4080,7 +4078,7 @@ }, "node_modules/hosted-git-info": { "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", @@ -4093,7 +4091,7 @@ }, "node_modules/hosted-git-info/node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", @@ -4106,14 +4104,14 @@ }, "node_modules/hosted-git-info/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/htmlparser2": { "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ @@ -4133,7 +4131,7 @@ }, "node_modules/htmlparser2/node_modules/entities": { "version": "7.0.1", - "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", @@ -4146,7 +4144,7 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", @@ -4160,7 +4158,7 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", @@ -4174,7 +4172,7 @@ }, "node_modules/husky": { "version": "9.1.7", - "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", @@ -4190,7 +4188,7 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", @@ -4203,7 +4201,7 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ @@ -4225,7 +4223,7 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", @@ -4235,7 +4233,7 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", @@ -4252,7 +4250,7 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", @@ -4262,7 +4260,7 @@ }, "node_modules/indent-string": { "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "license": "MIT", "engines": { @@ -4274,7 +4272,7 @@ }, "node_modules/index-to-position": { "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/index-to-position/-/index-to-position-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", "dev": true, "license": "MIT", @@ -4287,7 +4285,7 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC", @@ -4295,16 +4293,16 @@ }, "node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, "license": "ISC", "optional": true }, "node_modules/ink": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/ink/-/ink-7.0.6.tgz", - "integrity": "sha512-/KG651f+LHln9gumb5ltieFqzNGJdhX1b/WwsCUd2Py7Htuk9KUzyFrk25ugmzjXyDneXSoXD3cm4ql4dWFGsQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-7.1.0.tgz", + "integrity": "sha512-VWE6/yeLtFCJBNLflyI2OSylyXK1Rc24LuXup8Qt+icwkmmycFNdbn8IkSp6Frc0h1iA0NOvvi1ajW44U/w3Qg==", "license": "MIT", "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", @@ -4352,7 +4350,7 @@ }, "node_modules/ink-gradient": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/ink-gradient/-/ink-gradient-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/ink-gradient/-/ink-gradient-4.0.1.tgz", "integrity": "sha512-0ckdiM84zkfCdnTtcnq4BS3egIhUPPDoCqSx/7NUFsAVooBbdRuGnnWpk0fuaOTqU6rlZRh9F4LN1UI8fxd81Q==", "license": "MIT", "dependencies": { @@ -4373,7 +4371,7 @@ }, "node_modules/ink/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { @@ -4385,7 +4383,7 @@ }, "node_modules/ink/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { @@ -4395,93 +4393,9 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/ink/node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ink/node_modules/cli-truncate": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-6.0.0.tgz", - "integrity": "sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==", - "license": "MIT", - "dependencies": { - "slice-ansi": "^9.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ink/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ink/node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ink/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/ink/node_modules/slice-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-9.0.0.tgz", - "integrity": "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/is-docker": { "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", @@ -4497,7 +4411,7 @@ }, "node_modules/is-extendable": { "version": "0.1.1", - "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "license": "MIT", "engines": { @@ -4506,7 +4420,7 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", @@ -4516,7 +4430,7 @@ }, "node_modules/is-fullwidth-code-point": { "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "license": "MIT", "dependencies": { @@ -4531,7 +4445,7 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", @@ -4544,7 +4458,7 @@ }, "node_modules/is-in-ci": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/is-in-ci/-/is-in-ci-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", "license": "MIT", "bin": { @@ -4559,7 +4473,7 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", @@ -4578,7 +4492,7 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", @@ -4588,7 +4502,7 @@ }, "node_modules/is-wsl": { "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", @@ -4604,14 +4518,14 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/istextorbinary": { "version": "9.5.0", - "resolved": "https://registry.npmmirror.com/istextorbinary/-/istextorbinary-9.5.0.tgz", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", "dev": true, "license": "Artistic-2.0", @@ -4629,14 +4543,14 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.2.0", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.2.0.tgz", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, "funding": [ @@ -4659,7 +4573,7 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", @@ -4672,28 +4586,28 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", @@ -4706,14 +4620,14 @@ }, "node_modules/jsonc-parser": { "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true, "license": "MIT" }, "node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -4726,7 +4640,7 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.3", - "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "dev": true, "license": "MIT", @@ -4749,7 +4663,7 @@ }, "node_modules/jsonwebtoken/node_modules/semver": { "version": "7.8.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", @@ -4762,7 +4676,7 @@ }, "node_modules/jwa": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "license": "MIT", @@ -4774,7 +4688,7 @@ }, "node_modules/jws": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "license": "MIT", @@ -4785,7 +4699,7 @@ }, "node_modules/keytar": { "version": "7.9.0", - "resolved": "https://registry.npmmirror.com/keytar/-/keytar-7.9.0.tgz", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, @@ -4798,7 +4712,7 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", @@ -4808,7 +4722,7 @@ }, "node_modules/kind-of": { "version": "6.0.3", - "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "license": "MIT", "engines": { @@ -4817,7 +4731,7 @@ }, "node_modules/leven": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", @@ -4827,7 +4741,7 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", @@ -4841,7 +4755,7 @@ }, "node_modules/linkify-it": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", "funding": [ { @@ -4860,7 +4774,7 @@ }, "node_modules/lint-staged": { "version": "17.0.7", - "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-17.0.7.tgz", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz", "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", "dev": true, "license": "MIT", @@ -4885,7 +4799,7 @@ }, "node_modules/listr2": { "version": "10.2.1", - "resolved": "https://registry.npmmirror.com/listr2/-/listr2-10.2.1.tgz", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", "dev": true, "license": "MIT", @@ -4900,9 +4814,56 @@ "node": ">=22.13.0" } }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", @@ -4918,77 +4879,77 @@ }, "node_modules/lodash": { "version": "4.18.1", - "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, "node_modules/lodash.includes": { "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "dev": true, "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "dev": true, "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "dev": true, "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "dev": true, "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true, "license": "MIT" }, "node_modules/lodash.truncate": { "version": "4.4.2", - "resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true, "license": "MIT" }, "node_modules/log-update": { "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", @@ -5008,7 +4969,7 @@ }, "node_modules/log-update/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", @@ -5019,9 +4980,71 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.2", - "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "license": "MIT", @@ -5038,7 +5061,7 @@ }, "node_modules/log-update/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", @@ -5056,7 +5079,7 @@ }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "9.0.2", - "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", @@ -5074,7 +5097,7 @@ }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", @@ -5084,7 +5107,7 @@ }, "node_modules/markdown-it": { "version": "14.2.0", - "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.2.0.tgz", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", "funding": [ { @@ -5111,7 +5134,7 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", @@ -5121,13 +5144,13 @@ }, "node_modules/mdurl": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", @@ -5137,7 +5160,7 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", @@ -5151,7 +5174,7 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.2", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", @@ -5164,7 +5187,7 @@ }, "node_modules/mime": { "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "license": "MIT", @@ -5177,7 +5200,7 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", @@ -5187,7 +5210,7 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", @@ -5200,7 +5223,7 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "license": "MIT", "engines": { @@ -5209,7 +5232,7 @@ }, "node_modules/mimic-function": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", @@ -5222,7 +5245,7 @@ }, "node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", @@ -5236,7 +5259,7 @@ }, "node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", @@ -5249,7 +5272,7 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", @@ -5260,7 +5283,7 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", @@ -5270,7 +5293,7 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true, "license": "MIT", @@ -5278,36 +5301,36 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", - "resolved": "https://registry.npmmirror.com/mute-stream/-/mute-stream-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true, "license": "ISC" }, "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "dev": true, "license": "MIT", "optional": true }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/node-abi": { "version": "3.92.0", - "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.92.0.tgz", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "dev": true, "license": "MIT", @@ -5321,7 +5344,7 @@ }, "node_modules/node-abi/node_modules/semver": { "version": "7.8.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", @@ -5335,7 +5358,7 @@ }, "node_modules/node-addon-api": { "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-4.3.0.tgz", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "license": "MIT", @@ -5343,7 +5366,7 @@ }, "node_modules/node-releases": { "version": "2.0.47", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.47.tgz", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", "dev": true, "license": "MIT", @@ -5353,7 +5376,7 @@ }, "node_modules/node-sarif-builder": { "version": "3.4.0", - "resolved": "https://registry.npmmirror.com/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", "dev": true, "license": "MIT", @@ -5367,7 +5390,7 @@ }, "node_modules/normalize-package-data": { "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, "license": "BSD-2-Clause", @@ -5382,7 +5405,7 @@ }, "node_modules/normalize-package-data/node_modules/hosted-git-info": { "version": "7.0.2", - "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, "license": "ISC", @@ -5395,14 +5418,14 @@ }, "node_modules/normalize-package-data/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/normalize-package-data/node_modules/semver": { "version": "7.8.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", @@ -5415,7 +5438,7 @@ }, "node_modules/nth-check": { "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "license": "BSD-2-Clause", @@ -5428,7 +5451,7 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", @@ -5441,7 +5464,7 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", @@ -5451,16 +5474,15 @@ } }, "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=18" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5468,7 +5490,7 @@ }, "node_modules/open": { "version": "10.2.0", - "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", @@ -5486,9 +5508,9 @@ } }, "node_modules/openai": { - "version": "6.42.0", - "resolved": "https://registry.npmmirror.com/openai/-/openai-6.42.0.tgz", - "integrity": "sha512-1WFEt/uXMXOLhYRNkgJWo08Y2YNvNwpVU72K7ibrWgWpNOXd4VojXLbe6SQ4bLiUQ3Y8jz4IiyVkylJCL1DtZg==", + "version": "6.44.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz", + "integrity": "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==", "license": "Apache-2.0", "peerDependencies": { "ws": "^8.18.0", @@ -5505,7 +5527,7 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", @@ -5523,7 +5545,7 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", @@ -5539,7 +5561,7 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", @@ -5555,7 +5577,7 @@ }, "node_modules/p-map": { "version": "7.0.4", - "resolved": "https://registry.npmmirror.com/p-map/-/p-map-7.0.4.tgz", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", @@ -5568,7 +5590,7 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", @@ -5581,7 +5603,7 @@ }, "node_modules/parse-json": { "version": "8.3.0", - "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-8.3.0.tgz", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "dev": true, "license": "MIT", @@ -5599,7 +5621,7 @@ }, "node_modules/parse-json/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "license": "(MIT OR CC0-1.0)", @@ -5612,7 +5634,7 @@ }, "node_modules/parse-semver": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/parse-semver/-/parse-semver-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "license": "MIT", @@ -5622,7 +5644,7 @@ }, "node_modules/parse-semver/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", @@ -5632,7 +5654,7 @@ }, "node_modules/parse5": { "version": "7.3.0", - "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", @@ -5645,7 +5667,7 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.1.0", - "resolved": "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "license": "MIT", @@ -5659,7 +5681,7 @@ }, "node_modules/parse5-parser-stream": { "version": "7.1.2", - "resolved": "https://registry.npmmirror.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "license": "MIT", @@ -5672,7 +5694,7 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", @@ -5685,7 +5707,7 @@ }, "node_modules/patch-console": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/patch-console/-/patch-console-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", "license": "MIT", "engines": { @@ -5694,7 +5716,7 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", @@ -5704,7 +5726,7 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", @@ -5714,7 +5736,7 @@ }, "node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", @@ -5731,7 +5753,7 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "11.5.1", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, "license": "BlueOak-1.0.0", @@ -5741,7 +5763,7 @@ }, "node_modules/path-type": { "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/path-type/-/path-type-6.0.0.tgz", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "dev": true, "license": "MIT", @@ -5754,21 +5776,21 @@ }, "node_modules/pend": { "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", @@ -5781,7 +5803,7 @@ }, "node_modules/pluralize": { "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/pluralize/-/pluralize-8.0.0.tgz", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, "license": "MIT", @@ -5790,9 +5812,10 @@ } }, "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "dev": true, "license": "MIT", "optional": true, @@ -5802,7 +5825,7 @@ "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", + "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", @@ -5819,7 +5842,7 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", @@ -5829,7 +5852,7 @@ }, "node_modules/prettier": { "version": "3.8.4", - "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.4.tgz", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", @@ -5845,7 +5868,7 @@ }, "node_modules/pump": { "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", @@ -5857,7 +5880,7 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", @@ -5867,7 +5890,7 @@ }, "node_modules/punycode.js": { "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "license": "MIT", "engines": { @@ -5876,7 +5899,7 @@ }, "node_modules/qs": { "version": "6.15.2", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", @@ -5892,7 +5915,7 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ @@ -5913,7 +5936,7 @@ }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", @@ -5930,7 +5953,7 @@ }, "node_modules/rc-config-loader": { "version": "4.1.4", - "resolved": "https://registry.npmmirror.com/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", "dev": true, "license": "MIT", @@ -5943,7 +5966,7 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", @@ -5954,7 +5977,7 @@ }, "node_modules/react": { "version": "19.2.7", - "resolved": "https://registry.npmmirror.com/react/-/react-19.2.7.tgz", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { @@ -5963,7 +5986,7 @@ }, "node_modules/react-reconciler": { "version": "0.33.0", - "resolved": "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.33.0.tgz", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", "license": "MIT", "dependencies": { @@ -5978,7 +6001,7 @@ }, "node_modules/read": { "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/read/-/read-1.0.7.tgz", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dev": true, "license": "ISC", @@ -5991,7 +6014,7 @@ }, "node_modules/read-pkg": { "version": "9.0.1", - "resolved": "https://registry.npmmirror.com/read-pkg/-/read-pkg-9.0.1.tgz", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "dev": true, "license": "MIT", @@ -6011,7 +6034,7 @@ }, "node_modules/read-pkg/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "license": "(MIT OR CC0-1.0)", @@ -6024,7 +6047,7 @@ }, "node_modules/read-pkg/node_modules/unicorn-magic": { "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, "license": "MIT", @@ -6037,7 +6060,7 @@ }, "node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", @@ -6053,7 +6076,7 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", @@ -6063,7 +6086,7 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", @@ -6072,17 +6095,16 @@ } }, "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "license": "MIT", "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=18" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6090,7 +6112,7 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", @@ -6101,14 +6123,14 @@ }, "node_modules/rfdc": { "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true, "license": "MIT" }, "node_modules/run-applescript": { "version": "7.1.0", - "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", @@ -6121,7 +6143,7 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ @@ -6145,7 +6167,7 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ @@ -6166,14 +6188,14 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/sax": { "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, "license": "BlueOak-1.0.0", @@ -6183,13 +6205,13 @@ }, "node_modules/scheduler": { "version": "0.27.0", - "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/secretlint": { "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/secretlint/-/secretlint-10.2.2.tgz", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", "dev": true, "license": "MIT", @@ -6211,7 +6233,7 @@ }, "node_modules/section-matter": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", "license": "MIT", "dependencies": { @@ -6224,7 +6246,7 @@ }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", @@ -6234,7 +6256,7 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", @@ -6247,7 +6269,7 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", @@ -6257,7 +6279,7 @@ }, "node_modules/side-channel": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", @@ -6277,7 +6299,7 @@ }, "node_modules/side-channel-list": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", @@ -6294,7 +6316,7 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", @@ -6313,7 +6335,7 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", @@ -6332,21 +6354,14 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/simple-concat": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "dev": true, "funding": [ @@ -6368,7 +6383,7 @@ }, "node_modules/simple-get": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, "funding": [ @@ -6395,7 +6410,7 @@ }, "node_modules/slash": { "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/slash/-/slash-5.1.0.tgz", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "license": "MIT", @@ -6407,17 +6422,16 @@ } }, "node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.0.tgz", + "integrity": "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=20" + "node": ">=22" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" @@ -6425,9 +6439,8 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6438,7 +6451,7 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", @@ -6449,14 +6462,14 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmmirror.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", @@ -6467,20 +6480,20 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.23", - "resolved": "https://registry.npmmirror.com/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, "node_modules/stack-utils": { "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "license": "MIT", "dependencies": { @@ -6492,7 +6505,7 @@ }, "node_modules/stack-utils/node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "license": "MIT", "engines": { @@ -6501,7 +6514,7 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", @@ -6512,7 +6525,7 @@ }, "node_modules/string-argv": { "version": "0.3.2", - "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, "license": "MIT", @@ -6522,7 +6535,7 @@ }, "node_modules/string-width": { "version": "8.2.1", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "license": "MIT", "dependencies": { @@ -6538,7 +6551,7 @@ }, "node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { @@ -6553,7 +6566,7 @@ }, "node_modules/strip-bom-string": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", "license": "MIT", "engines": { @@ -6562,7 +6575,7 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", @@ -6575,7 +6588,7 @@ }, "node_modules/structured-source": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/structured-source/-/structured-source-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", "dev": true, "license": "BSD-2-Clause", @@ -6585,7 +6598,7 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", @@ -6598,7 +6611,7 @@ }, "node_modules/supports-hyperlinks": { "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", @@ -6615,7 +6628,7 @@ }, "node_modules/table": { "version": "6.9.0", - "resolved": "https://registry.npmmirror.com/table/-/table-6.9.0.tgz", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "license": "BSD-3-Clause", @@ -6632,7 +6645,7 @@ }, "node_modules/table/node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", @@ -6649,7 +6662,7 @@ }, "node_modules/table/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", @@ -6659,14 +6672,14 @@ }, "node_modules/table/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/table/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", @@ -6676,14 +6689,14 @@ }, "node_modules/table/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/table/node_modules/slice-ansi": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", @@ -6701,7 +6714,7 @@ }, "node_modules/table/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", @@ -6716,7 +6729,7 @@ }, "node_modules/table/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", @@ -6729,7 +6742,7 @@ }, "node_modules/tagged-tag": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", "license": "MIT", "engines": { @@ -6741,7 +6754,7 @@ }, "node_modules/tar-fs": { "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", @@ -6755,7 +6768,7 @@ }, "node_modules/tar-stream": { "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", @@ -6773,7 +6786,7 @@ }, "node_modules/terminal-link": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/terminal-link/-/terminal-link-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", "dev": true, "license": "MIT", @@ -6790,7 +6803,7 @@ }, "node_modules/terminal-size": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/terminal-size/-/terminal-size-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", "license": "MIT", "engines": { @@ -6802,14 +6815,14 @@ }, "node_modules/text-table": { "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "license": "MIT" }, "node_modules/textextensions": { "version": "6.11.0", - "resolved": "https://registry.npmmirror.com/textextensions/-/textextensions-6.11.0.tgz", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", "dev": true, "license": "Artistic-2.0", @@ -6825,13 +6838,13 @@ }, "node_modules/tinycolor2": { "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "license": "MIT" }, "node_modules/tinyexec": { "version": "1.2.4", - "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", @@ -6841,7 +6854,7 @@ }, "node_modules/tinyglobby": { "version": "0.2.17", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", @@ -6858,7 +6871,7 @@ }, "node_modules/tinygradient": { "version": "1.1.5", - "resolved": "https://registry.npmmirror.com/tinygradient/-/tinygradient-1.1.5.tgz", + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", "license": "MIT", "dependencies": { @@ -6868,7 +6881,7 @@ }, "node_modules/tmp": { "version": "0.2.7", - "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", @@ -6878,7 +6891,7 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", @@ -6891,7 +6904,7 @@ }, "node_modules/ts-api-utils": { "version": "2.5.0", - "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", @@ -6904,14 +6917,14 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, "node_modules/tsx": { "version": "4.22.4", - "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", @@ -6930,7 +6943,7 @@ }, "node_modules/tunnel": { "version": "0.0.6", - "resolved": "https://registry.npmmirror.com/tunnel/-/tunnel-0.0.6.tgz", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, "license": "MIT", @@ -6940,7 +6953,7 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "license": "Apache-2.0", @@ -6954,7 +6967,7 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", @@ -6967,7 +6980,7 @@ }, "node_modules/type-fest": { "version": "5.7.0", - "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-5.7.0.tgz", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", "integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==", "license": "(MIT OR CC0-1.0)", "dependencies": { @@ -6982,7 +6995,7 @@ }, "node_modules/typed-rest-client": { "version": "1.8.11", - "resolved": "https://registry.npmmirror.com/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "license": "MIT", @@ -6994,7 +7007,7 @@ }, "node_modules/typescript": { "version": "6.0.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", @@ -7007,16 +7020,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.61.0", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz", - "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.61.0", - "@typescript-eslint/parser": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/utils": "8.61.0" + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7032,21 +7045,21 @@ }, "node_modules/uc.micro": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, "node_modules/underscore": { "version": "1.13.8", - "resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.8.tgz", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true, "license": "MIT" }, "node_modules/undici": { - "version": "7.27.2", - "resolved": "https://registry.npmmirror.com/undici/-/undici-7.27.2.tgz", - "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -7054,14 +7067,14 @@ }, "node_modules/undici-types": { "version": "7.24.6", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.3.0", - "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", @@ -7074,7 +7087,7 @@ }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -7084,7 +7097,7 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ @@ -7115,7 +7128,7 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", @@ -7125,14 +7138,14 @@ }, "node_modules/url-join": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/url-join/-/url-join-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true, "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT", @@ -7140,7 +7153,7 @@ }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", @@ -7151,7 +7164,7 @@ }, "node_modules/version-range": { "version": "4.15.0", - "resolved": "https://registry.npmmirror.com/version-range/-/version-range-4.15.0.tgz", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", "dev": true, "license": "Artistic-2.0", @@ -7164,7 +7177,7 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, @@ -7178,7 +7191,7 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", @@ -7188,7 +7201,7 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", @@ -7204,7 +7217,7 @@ }, "node_modules/widest-line": { "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/widest-line/-/widest-line-6.0.0.tgz", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz", "integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==", "license": "MIT", "dependencies": { @@ -7219,7 +7232,7 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", @@ -7229,7 +7242,7 @@ }, "node_modules/wrap-ansi": { "version": "10.0.0", - "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", "license": "MIT", "dependencies": { @@ -7246,7 +7259,7 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { @@ -7258,7 +7271,7 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC", @@ -7266,7 +7279,7 @@ }, "node_modules/ws": { "version": "8.21.0", - "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { @@ -7287,7 +7300,7 @@ }, "node_modules/wsl-utils": { "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "dev": true, "license": "MIT", @@ -7303,7 +7316,7 @@ }, "node_modules/xml2js": { "version": "0.5.0", - "resolved": "https://registry.npmmirror.com/xml2js/-/xml2js-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "license": "MIT", @@ -7317,7 +7330,7 @@ }, "node_modules/xmlbuilder": { "version": "11.0.1", - "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, "license": "MIT", @@ -7327,14 +7340,14 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yaml": { "version": "2.9.0", - "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", @@ -7351,7 +7364,7 @@ }, "node_modules/yauzl": { "version": "3.4.0", - "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-3.4.0.tgz", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "dev": true, "license": "MIT", @@ -7364,7 +7377,7 @@ }, "node_modules/yazl": { "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/yazl/-/yazl-2.5.1.tgz", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", "dev": true, "license": "MIT", @@ -7374,7 +7387,7 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", @@ -7387,13 +7400,13 @@ }, "node_modules/yoga-layout": { "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/yoga-layout/-/yoga-layout-3.2.1.tgz", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", "license": "MIT" }, "node_modules/zod": { "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { @@ -7402,7 +7415,7 @@ }, "node_modules/zod-validation-error": { "version": "4.0.2", - "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", "dev": true, "license": "MIT", @@ -7435,7 +7448,7 @@ }, "packages/cli/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { @@ -7447,7 +7460,7 @@ }, "packages/cli/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", "engines": { @@ -7470,7 +7483,7 @@ }, "packages/core/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { @@ -7482,31 +7495,13 @@ }, "packages/core/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", "engines": { "node": ">= 4" } }, - "packages/vscode": { - "name": "@vegamo/deepcode-vscode", - "version": "0.1.22", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@vegamo/deepcode-core": "*", - "markdown-it": "^14.1.1", - "openai": "^6.35.0" - }, - "devDependencies": { - "@types/markdown-it": "^14.1.1", - "@types/vscode": "^1.85.0" - }, - "engines": { - "vscode": "^1.85.0" - } - }, "packages/vscode-ide-companion": { "name": "deepcode-vscode", "version": "0.1.22", diff --git a/scripts/clean.js b/scripts/clean.js index db532c3b..4fd4c7e6 100644 --- a/scripts/clean.js +++ b/scripts/clean.js @@ -9,31 +9,39 @@ const RMRF = { recursive: true, force: true }; console.log("Cleaning build artifacts...\n"); -// Root artifacts +// Root node_modules rmSync(join(root, "node_modules"), RMRF); console.log(" rm node_modules/"); -// Generated version files -for (const pkg of ["cli", "core", "vscode-ide-companion"]) { - rmSync(join(root, "packages", pkg, "src", "generated"), RMRF); - console.log(` rm packages/${pkg}/src/generated/`); -} - -// All workspace dist/ and tsbuildinfo +// Per-package node_modules, dist, generated, tsbuildinfo const packageDirs = globSync("packages/*", { cwd: root, absolute: true }); for (const pkgDir of packageDirs) { + const short = pkgDir.replace(root + "/", ""); + + rmSync(join(pkgDir, "node_modules"), RMRF); + console.log(` rm ${short}/node_modules/`); + rmSync(join(pkgDir, "dist"), RMRF); - console.log(` rm ${join(pkgDir, "dist")}`); + console.log(` rm ${short}/dist/`); + + rmSync(join(pkgDir, "src", "generated"), RMRF); + console.log(` rm ${short}/src/generated/`); + rmSync(join(pkgDir, "tsconfig.tsbuildinfo"), { force: true }); } -// Clean up vscode-ide-companion package -rmSync(join(root, "packages/vscode-ide-companion/node_modules"), RMRF); -// VSCode .vsix files -const vsixFiles = globSync("packages/vscode-ide-companion/*.vsix", { cwd: root }); +// VSCode companion specific artifacts +const vscodeDir = join(root, "packages", "vscode-ide-companion"); +rmSync(join(vscodeDir, "out"), RMRF); +console.log(" rm packages/vscode-ide-companion/out/"); + +rmSync(join(vscodeDir, "templates"), RMRF); +console.log(" rm packages/vscode-ide-companion/templates/"); + +const vsixFiles = globSync("*.vsix", { cwd: vscodeDir }); for (const vsixFile of vsixFiles) { - rmSync(join(root, vsixFile), RMRF); - console.log(` rm ${vsixFile}`); + rmSync(join(vscodeDir, vsixFile), RMRF); + console.log(` rm packages/vscode-ide-companion/${vsixFile}`); } console.log("\n✅ Clean complete.\n\n"); From d83c348912fb9f6e6d14ede960ab4eecd20287c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:24:40 +0000 Subject: [PATCH 177/212] chore(deps): bump undici from 7.25.0 to 7.28.0 Bumps [undici](https://github.com/nodejs/undici) from 7.25.0 to 7.28.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.25.0...v7.28.0) --- updated-dependencies: - dependency-name: undici dependency-version: 7.28.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8db59638..a65e91ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3591,9 +3591,9 @@ } }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmmirror.com/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" From efdacc23190c4d95ce99c6994c73d3774e3bc3f2 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 22 Jun 2026 15:34:28 +0800 Subject: [PATCH 178/212] =?UTF-8?q?build(core):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=90=8E=E6=9E=84=E5=BB=BA=E7=9A=84=20ESM=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E5=90=8D=E9=87=8D=E5=86=99=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在构建流程中新增运行 rewrite-esm-imports.js 脚本步骤 - 该脚本修正 core 包 dist 目录下无扩展名的相对导入为显式 .js 扩展名 - 更新 esbuild 配置,将 @vegamo/deepcode-core 标记为外部依赖 - 保证 Node.js ESM 可以正确解析导入路径,避免运行时错误 --- scripts/build.js | 5 +++-- scripts/esbuild.config.js | 1 + scripts/rewrite-esm-imports.js | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 scripts/rewrite-esm-imports.js diff --git a/scripts/build.js b/scripts/build.js index 080e2e54..7710968b 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -17,7 +17,8 @@ console.log("========================================="); console.log(" Deep Code CLI — Build"); console.log("========================================="); -run("npm", ["run", "build", "--workspace=@vegamo/deepcode-core"], "1/2"); -run("npm", ["run", "bundle"], "2/2"); +run("npm", ["run", "build", "--workspace=@vegamo/deepcode-core"], "1/3"); +run("node", ["scripts/rewrite-esm-imports.js"], "2/3"); +run("npm", ["run", "bundle"], "3/3"); console.log("\n✅ Build complete.\n\n"); diff --git a/scripts/esbuild.config.js b/scripts/esbuild.config.js index bf814a32..36c174dc 100644 --- a/scripts/esbuild.config.js +++ b/scripts/esbuild.config.js @@ -20,6 +20,7 @@ await build({ jsx: "automatic", jsxImportSource: "react", packages: "external", + external: ["@vegamo/deepcode-core"], logOverride: { "empty-import-meta": "silent", }, diff --git a/scripts/rewrite-esm-imports.js b/scripts/rewrite-esm-imports.js new file mode 100644 index 00000000..715e5c3b --- /dev/null +++ b/scripts/rewrite-esm-imports.js @@ -0,0 +1,40 @@ +/** + * Post-build script: rewrites extensionless relative imports in the core + * package's dist/ output to include explicit ".js" extensions. + * + * tsc with moduleResolution:"bundler" emits `from "./foo"` (no extension). + * Node.js ESM requires `from "./foo.js"`. This script bridges the gap. + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { globSync } from "glob"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); +const distDir = join(root, "packages", "core", "dist"); + +const files = globSync("**/*.js", { cwd: distDir, absolute: true }); + +// Match: from "./anything" or from "../anything" +// Negative lookahead: skip if already ends with .js, .json, .node, or is a bare specifier +const IMPORT_RE = /(from\s+["'])(\.\.?\/[^"']+?)(? { + rewrites++; + return `${prefix}${specifier}.js${quote}`; + }); + + if (rewrites > 0) { + writeFileSync(filePath, updated, "utf8"); + totalRewrites += rewrites; + } +} + +console.log(`\n✅ Rewrote ${totalRewrites} imports across ${files.length} files in core/dist/\n`); From 93ebf7cc241ffd21d01bafd2f9d4d267f9bf3ba2 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 22 Jun 2026 20:48:38 +0800 Subject: [PATCH 179/212] =?UTF-8?q?build(cli):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E6=B5=81=E7=A8=8B=E5=B9=B6=E5=86=85=E8=81=94?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E5=8F=8A=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 CLI 构建改为使用 esbuild 的拆分和代码分块,适配 Node 22 环境 - esbuild 配置中内联 core 包及所有依赖,实现零运行时依赖 - 引入自定义 shims 支持 Node 内置模块和空 shim 替代浏览器专用包 - 修改 copy 脚本,复制 core/templates 至 dist/templates 及 legacy bundled 目录 - 发布脚本重构:只发布单个打包的 CLI 包,不再单独发布 core 包 - 发布时在 dist 写入无依赖的 package.json 并从 dist 目录发布 - 简化发布流程,减少步骤数并更新发布文档说明 - package.json 添加对 react-devtools-core 的开发依赖用于本地开发支持 --- .deepcode/AGENTS.md | 4 +- RELEASE.md | 15 ++-- RELEASE_en.md | 15 ++-- package-lock.json | 47 +++++++++++ package.json | 1 + scripts/copy-bundle-assets.js | 34 ++++++-- scripts/empty-shim.js | 2 + scripts/esbuild-shims.js | 22 +++++ scripts/esbuild.config.js | 24 ++++-- scripts/prepare-package.js | 150 ++++++++++++++++++---------------- 10 files changed, 213 insertions(+), 101 deletions(-) create mode 100644 scripts/empty-shim.js create mode 100644 scripts/esbuild-shims.js diff --git a/.deepcode/AGENTS.md b/.deepcode/AGENTS.md index 167d5a68..9d827b0e 100644 --- a/.deepcode/AGENTS.md +++ b/.deepcode/AGENTS.md @@ -68,8 +68,8 @@ dist/ # Bundled CLI output (gitignored) | `npm run format` | Prettier on all `src/**/*.{ts,tsx}` | | `npm run format:check` | Prettier in check-only mode | | `npm run check` | Runs typecheck + lint + format:check together | -| `npm run bundle` | esbuild bundles `src/cli.tsx` → `dist/cli.js` (ESM, Node 18) | -| `npm run build` | `check` + `bundle` + chmod 755 — full CI gate before publish | +| `npm run bundle` | esbuild bundles cli + core + all deps → `dist/cli.js` (ESM, Node 22) | +| `npm run build` | build core (tsc) + rewrite ESM imports + bundle CLI (esbuild) | | `npm test` | Runs all tests via `node src/tests/run-tests.mjs` | | `npm run test:single -- ` | Run a single test file via `tsx --test` (e.g., `npm run test:single -- src/tests/session.test.ts`) | diff --git a/RELEASE.md b/RELEASE.md index deca0fd7..19229167 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -105,7 +105,7 @@ git tag v0.1.32 ## prepare:package — 构建并发布到 npm -完成质量检查、构建、发布两个 npm 包,并自动创建 git commit 和 tag。 +完成质量检查、构建、发布 CLI 到 npm,并自动创建 git commit 和 tag。 ### 基本用法 @@ -122,7 +122,7 @@ npm run prepare:package -- [options] | `--dry-run` | 预演模式,不实际执行任何写操作 | | `--force` | 跳过 main 分支检查,允许从其他分支发布 | -### 执行流程(9 步) +### 执行流程(8 步) | 步骤 | 操作 | 说明 | |------|------|------| @@ -131,10 +131,9 @@ npm run prepare:package -- [options] | 3 | 更新版本号 | 同时更新 `packages/core` 和 `packages/cli` 的 version | | 4 | 质量检查 | `npm run check`(typecheck + eslint + prettier) | | 5 | 测试 | `npm run test --workspaces` | -| 6 | 构建 | `npm run build`(core tsc + cli esbuild bundle) | -| 7 | 发布 core | `npm publish --workspace=@vegamo/deepcode-core --access public` | -| 8 | 发布 cli | 将 cli 的 `@vegamo/deepcode-core` 依赖从 `file:../core` 临时改为 `^`,发布后恢复 | -| 9 | Git commit & tag | `chore(release): v` + `git tag v` | +| 6 | 构建 | `npm run build`(core tsc + esbuild 将 core 及所有依赖内联到 `dist/cli.js`) | +| 7 | 发布 CLI | 往 `dist/` 写入 `dependencies: {}` 的 package.json,从 `dist/` 目录执行 `npm publish` | +| 8 | Git commit & tag | `chore(release): v` + `git tag v` | ### 完整示例 @@ -152,9 +151,9 @@ npm run prepare:package -- 0.1.32 --dry-run npm run prepare:package -- 0.1.32 --force ``` -### 关于 file:../core 依赖 +### 关于 Core 打包策略 -CLI 包的 `@vegamo/deepcode-core` 依赖在开发时使用 `"file:../core"`(monorepo 本地链接)。发布到 npm 时,脚本会自动将其替换为 `"^"`,发布完成后恢复为 `file:../core`。这个过程对用户透明,无需手动处理。 +CLI 的 `package.json` 中保留 `"@vegamo/deepcode-core": "file:../core"` 用于本地开发(IDE 类型检查、monorepo 工作区解析)。构建时 esbuild 使用 `packages: "bundle"` 将 core 的全部代码及其运行时依赖(`openai`、`ejs`、`zod` 等)内联到单个 `dist/cli.js` 文件中。发布时脚本往 `dist/` 写入 `dependencies: {}` 的 `package.json`,从 `dist/` 目录发布,因此发布的 CLI 包零运行时依赖。`@vegamo/deepcode-core` 不再作为独立 npm 包发布。 ### 发布后 diff --git a/RELEASE_en.md b/RELEASE_en.md index 4844bf70..e6cd7013 100644 --- a/RELEASE_en.md +++ b/RELEASE_en.md @@ -105,7 +105,7 @@ git tag v0.1.32 ## prepare:package — Build and Publish to npm -Runs quality checks, builds, publishes both npm packages, and automatically creates a git commit with tag. +Runs quality checks, builds, publishes the CLI to npm, and automatically creates a git commit with tag. ### Basic Usage @@ -122,7 +122,7 @@ npm run prepare:package -- [options] | `--dry-run` | Preview mode, no actual writes | | `--force` | Skip main branch check, allow publishing from other branches | -### Execution Flow (9 Steps) +### Execution Flow (8 Steps) | Step | Action | Description | |------|--------|-------------| @@ -131,10 +131,9 @@ npm run prepare:package -- [options] | 3 | Update versions | Updates `packages/core` and `packages/cli` version fields | | 4 | Quality checks | `npm run check` (typecheck + eslint + prettier) | | 5 | Tests | `npm run test --workspaces` | -| 6 | Build | `npm run build` (core tsc + cli esbuild bundle) | -| 7 | Publish core | `npm publish --workspace=@vegamo/deepcode-core --access public` | -| 8 | Publish cli | Temporarily changes cli's `@vegamo/deepcode-core` dep from `file:../core` to `^`, restores after publish | -| 9 | Git commit & tag | `chore(release): v` + `git tag v` | +| 6 | Build | `npm run build` (core tsc + esbuild inlines core and all deps into `dist/cli.js`) | +| 7 | Publish CLI | Writes `dist/package.json` with `dependencies: {}`, runs `npm publish` from `dist/` | +| 8 | Git commit & tag | `chore(release): v` + `git tag v` | ### Examples @@ -152,9 +151,9 @@ npm run prepare:package -- 0.1.32 --dry-run npm run prepare:package -- 0.1.32 --force ``` -### About the file:../core Dependency +### About the Core Bundling Strategy -The CLI package uses `"file:../core"` for the `@vegamo/deepcode-core` dependency during development (monorepo local link). When publishing to npm, the script automatically replaces it with `"^"` and restores it after publishing. This is transparent — no manual handling required. +The CLI's `package.json` keeps `"@vegamo/deepcode-core": "file:../core"` for local development (IDE type checking, monorepo workspace resolution). At build time, esbuild uses `packages: "bundle"` to inline all of core's code and its runtime dependencies (`openai`, `ejs`, `zod`, etc.) into a single `dist/cli.js` file. At publish time, the script writes a `dist/package.json` with `dependencies: {}` and publishes from the `dist/` directory, so the published CLI package has zero runtime dependencies. `@vegamo/deepcode-core` is no longer published as a separate npm package. ### After Publishing diff --git a/package-lock.json b/package-lock.json index e36f1926..0e4b47f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "husky": "^9.1.7", "lint-staged": "^17.0.4", "prettier": "^3.8.3", + "react-devtools-core": "^7.0.1", "tsx": "^4.21.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2" @@ -5984,6 +5985,39 @@ "node": ">=0.10.0" } }, + "node_modules/react-devtools-core": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-7.0.1.tgz", + "integrity": "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/react-reconciler": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", @@ -6277,6 +6311,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", diff --git a/package.json b/package.json index 165ccfdb..c9a0e723 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "husky": "^9.1.7", "lint-staged": "^17.0.4", "prettier": "^3.8.3", + "react-devtools-core": "^7.0.1", "tsx": "^4.21.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2" diff --git a/scripts/copy-bundle-assets.js b/scripts/copy-bundle-assets.js index 88315484..a5b261aa 100644 --- a/scripts/copy-bundle-assets.js +++ b/scripts/copy-bundle-assets.js @@ -6,22 +6,42 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, ".."); const cliRoot = join(root, "packages", "cli"); const distDir = join(cliRoot, "dist"); -const bundledSkillsSrc = join(root, "packages", "core", "templates", "skills", "bundled"); -const bundledSkillsDest = join(distDir, "bundled"); + +// 1. Copy core/templates/ → dist/templates/ +// Core's getExtensionRoot() resolves to dist/ in the bundle (because +// __dirname is dist/chunks/ and ".." goes up to dist/). The runtime code +// loads templates from extensionRoot + "/templates/...", so they must +// exist at dist/templates/. +const templatesSrc = join(root, "packages", "core", "templates"); +const templatesDest = join(distDir, "templates"); if (!existsSync(distDir)) { mkdirSync(distDir, { recursive: true }); } -if (!existsSync(bundledSkillsSrc)) { - console.error(`Bundled skills directory not found at ${bundledSkillsSrc}`); +if (!existsSync(templatesSrc)) { + console.error(`Templates directory not found at ${templatesSrc}`); process.exit(1); } -rmSync(bundledSkillsDest, { recursive: true, force: true }); -cpSync(bundledSkillsSrc, bundledSkillsDest, { +rmSync(templatesDest, { recursive: true, force: true }); +cpSync(templatesSrc, templatesDest, { recursive: true, dereference: true, }); +console.log("\n✅ Copied core/templates/ → dist/templates/"); + +// 2. Copy bundled skills to dist/bundled/ (legacy path used by getBundledSkillsRoot fallback) +const bundledSkillsSrc = join(templatesSrc, "skills", "bundled"); +const bundledSkillsDest = join(distDir, "bundled"); + +if (existsSync(bundledSkillsSrc)) { + rmSync(bundledSkillsDest, { recursive: true, force: true }); + cpSync(bundledSkillsSrc, bundledSkillsDest, { + recursive: true, + dereference: true, + }); + console.log("✅ Copied bundled skills → dist/bundled/"); +} -console.log("\n✅ All bundle assets copied to dist/bundled/"); +console.log("\n✅ All bundle assets copied.\n"); diff --git a/scripts/empty-shim.js b/scripts/empty-shim.js new file mode 100644 index 00000000..76f9cbf5 --- /dev/null +++ b/scripts/empty-shim.js @@ -0,0 +1,2 @@ +// Empty shim for react-devtools-core (browser-only, not needed in CLI bundle) +export default {}; diff --git a/scripts/esbuild-shims.js b/scripts/esbuild-shims.js new file mode 100644 index 00000000..f38ee0c1 --- /dev/null +++ b/scripts/esbuild-shims.js @@ -0,0 +1,22 @@ +/** + * Shims for esbuild ESM bundles. + * + * When esbuild bundles CJS modules into ESM output, it replaces require() + * calls with a __require shim that throws for non-bundled modules. This + * file provides a real require() via createRequire() so Node.js built-in + * modules (assert, events, zlib, etc.) resolve correctly at runtime. + */ + +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +const _require = createRequire(import.meta.url); + +if (typeof globalThis.require === "undefined") { + globalThis.require = _require; +} + +export const require = _require; +export const __filename = fileURLToPath(import.meta.url); +export const __dirname = dirname(__filename); diff --git a/scripts/esbuild.config.js b/scripts/esbuild.config.js index 36c174dc..3236c5ee 100644 --- a/scripts/esbuild.config.js +++ b/scripts/esbuild.config.js @@ -7,23 +7,37 @@ const root = join(__dirname, ".."); const cliRoot = join(root, "packages", "cli"); const entry = join(cliRoot, "src", "cli.tsx"); -const outfile = join(cliRoot, "dist", "cli.js"); await build({ entryPoints: [entry], bundle: true, + outdir: join(cliRoot, "dist"), + entryNames: "[name]", + chunkNames: "chunks/[name]-[hash]", + splitting: true, platform: "node", format: "esm", target: "node22", - outfile, banner: { js: "#!/usr/bin/env node" }, jsx: "automatic", jsxImportSource: "react", - packages: "external", - external: ["@vegamo/deepcode-core"], + packages: "bundle", + inject: [join(__dirname, "esbuild-shims.js")], + alias: { + // react-devtools-core is a browser-only package pulled in by ink's + // devtools support. It cannot run in a Node.js CLI, so we replace it + // with an empty shim so esbuild doesn't bundle the real (broken) code. + "react-devtools-core": join(__dirname, "empty-shim.js"), + }, + define: { + "process.env.NODE_ENV": JSON.stringify("production"), + }, logOverride: { "empty-import-meta": "silent", }, + metafile: true, + write: true, + keepNames: true, }); -console.log(`\n✅ ${outfile} built successfully\n\n`); +console.log(`\n✅ ${join(cliRoot, "dist", "cli.js")} built successfully\n\n`); diff --git a/scripts/prepare-package.js b/scripts/prepare-package.js index 6049f082..02481c50 100644 --- a/scripts/prepare-package.js +++ b/scripts/prepare-package.js @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { readFileSync, writeFileSync } from "node:fs"; +import { readFileSync, writeFileSync, existsSync, copyFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -81,7 +81,7 @@ for (let i = 0; i < args.length; i++) { if (!version) { log(` -Usage: node scripts/publish.js [options] +Usage: node scripts/prepare-package.js [options] Arguments: Semver version to publish (e.g. 0.1.32, 0.2.0-beta.1) @@ -92,9 +92,9 @@ Options: --force Skip branch check (publish from non-main branch) Examples: - node scripts/publish.js 0.1.32 - node scripts/publish.js 0.1.32-beta.1 --tag beta - node scripts/publish.js 0.1.32 --dry-run + node scripts/prepare-package.js 0.1.32 + node scripts/prepare-package.js 0.1.32-beta.1 --tag beta + node scripts/prepare-package.js 0.1.32 --dry-run `); process.exit(1); } @@ -103,7 +103,7 @@ if (!isValidSemver(version)) { fail(`Invalid semver version: ${version}`); } -const TOTAL_STEPS = 9; +const TOTAL_STEPS = 8; // ── Banner ─────────────────────────────────────────────────────────────────── @@ -169,9 +169,6 @@ const cliPkg = readJson(cliPkgPath); const oldVersion = corePkg.version; -// Save originals for restore -const origCliPkg = JSON.stringify(cliPkg, null, 2) + "\n"; - corePkg.version = version; cliPkg.version = version; @@ -199,75 +196,88 @@ ok("All tests passed"); // ── 6. Build ───────────────────────────────────────────────────────────────── -step(6, TOTAL_STEPS, "Building packages..."); +step(6, TOTAL_STEPS, "Building and bundling packages..."); run("npm", ["run", "build"], { dryRun }); -ok("Build complete"); - -// ── 7. Publish core ────────────────────────────────────────────────────────── - -step(7, TOTAL_STEPS, "Publishing @vegamo/deepcode-core..."); - -const corePublishArgs = [ - "publish", - "--workspace=@vegamo/deepcode-core", - "--access", - "public", - "--tag", - tag, - "--registry", - "https://registry.npmjs.org", -]; -if (dryRun) corePublishArgs.push("--dry-run"); - -run("npm", corePublishArgs, { dryRun, label: `npm ${corePublishArgs.join(" ")}` }); -ok(`Published @vegamo/deepcode-core@${version}`); - -// ── 8. Patch CLI deps & publish ────────────────────────────────────────────── - -step(8, TOTAL_STEPS, "Patching CLI dependencies and publishing @vegamo/deepcode-cli..."); - -// Replace file:../core with ^version for npm -const patchedCliPkg = readJson(cliPkgPath); -const coreDep = patchedCliPkg.dependencies["@vegamo/deepcode-core"]; -if (coreDep && coreDep.startsWith("file:")) { - patchedCliPkg.dependencies["@vegamo/deepcode-core"] = `^${version}`; - if (!dryRun) { - writeJson(cliPkgPath, patchedCliPkg); +ok("Build and bundle complete"); + +// ── 7. Prepare dist/ for publishing ────────────────────────────────────────── + +step(7, TOTAL_STEPS, "Preparing dist/ for publishing..."); + +const cliRoot = join(root, "packages", "cli"); +const distDir = join(cliRoot, "dist"); +const distCliJs = join(distDir, "cli.js"); +const distChunks = join(distDir, "chunks"); +const distBundled = join(distDir, "bundled"); + +if (!existsSync(distCliJs)) { + fail(`Bundle artifact not found: ${distCliJs}. Run "npm run build" first.`); +} +if (!existsSync(distChunks)) { + fail(`Chunks directory not found: ${distChunks}. Run "npm run build" first.`); +} +if (!existsSync(distBundled)) { + fail(`Bundled assets not found: ${distBundled}. Run "npm run build" first.`); +} + +// Copy README.md and LICENSE into dist/ +for (const file of ["README.md", "LICENSE"]) { + const src = join(root, file); + const dest = join(distDir, file); + if (existsSync(src)) { + if (!dryRun) { + copyFileSync(src, dest); + } + log(` Copied ${file} → dist/`); + } else { + log(` Warning: ${file} not found at ${src}`); } - log(` Patched @vegamo/deepcode-core dep: "${coreDep}" → "^${version}"`); } -const cliPublishArgs = [ - "publish", - "--workspace=@vegamo/deepcode-cli", - "--access", - "public", - "--tag", - tag, - "--registry", - "https://registry.npmjs.org", -]; -if (dryRun) cliPublishArgs.push("--dry-run"); - -run("npm", cliPublishArgs, { dryRun, label: `npm ${cliPublishArgs.join(" ")}` }); -ok(`Published @vegamo/deepcode-cli@${version}`); +// Write a new package.json for publishing with empty dependencies. +// All runtime code (including @vegamo/deepcode-core and its deps) is already +// bundled into dist/cli.js by esbuild with packages: "bundle". +const distPackageJson = { + name: cliPkg.name, + version: version, + description: cliPkg.description, + license: cliPkg.license, + type: "module", + main: "cli.js", + bin: { + deepcode: "cli.js", + }, + files: ["cli.js", "chunks/**", "templates/**", "bundled/**", "README.md", "LICENSE"], + engines: cliPkg.engines, + dependencies: {}, +}; -// Restore file:../core for local development if (!dryRun) { - writeJson(cliPkgPath, JSON.parse(origCliPkg)); - // But keep the new version - const restoredCli = readJson(cliPkgPath); - restoredCli.version = version; - writeJson(cliPkgPath, restoredCli); - log(" Restored @vegamo/deepcode-core dep to file:../core"); + writeJson(join(distDir, "package.json"), distPackageJson); } +log(" Written dist/package.json with dependencies: {}"); + +ok("dist/ prepared for publishing"); + +// ── 7. Publish from dist/ ──────────────────────────────────────────────────── -// ── 9. Git commit + tag ────────────────────────────────────────────────────── +step(8, TOTAL_STEPS, "Publishing @vegamo/deepcode-cli from dist/..."); + +const publishArgs = ["publish", "--access", "public", "--tag", tag, "--registry", "https://registry.npmjs.org"]; +if (dryRun) publishArgs.push("--dry-run"); + +run("npm", publishArgs, { + dryRun, + cwd: distDir, + label: `cd dist && npm ${publishArgs.join(" ")}`, +}); +ok(`Published @vegamo/deepcode-cli@${version}`); -step(9, TOTAL_STEPS, "Creating git commit and tag..."); +// ── Git commit + tag ───────────────────────────────────────────────────────── if (!dryRun) { + log("\nCreating git commit and tag..."); run("git", ["add", "packages/core/package.json", "packages/cli/package.json"], { label: "git add packages/*/package.json", }); @@ -279,8 +289,7 @@ if (!dryRun) { }); ok(`Created commit and tag v${version}`); } else { - log(` (dry-run) git add + commit "chore(release): v${version}"`); - log(` (dry-run) git tag v${version}`); + log("\n (dry-run) git add + commit + tag"); } // ── Done ───────────────────────────────────────────────────────────────────── @@ -289,9 +298,8 @@ console.log("\n========================================="); console.log(` 🎉 Published v${version} successfully!`); console.log("========================================="); console.log(` - Packages published: - • @vegamo/deepcode-core@${version} - • @vegamo/deepcode-cli@${version} + Package published: + • @vegamo/deepcode-cli@${version} (core bundled, zero runtime dependencies) Verify: npm view @vegamo/deepcode-cli version From 78d9ec6cc5e9be2139b43a550434d7162694d33f Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 22 Jun 2026 21:11:05 +0800 Subject: [PATCH 180/212] =?UTF-8?q?fix(cli):=20=E8=B0=83=E6=95=B4=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=A4=8D=E5=88=B6=E9=80=BB=E8=BE=91=E4=BB=A5=E6=8E=92?= =?UTF-8?q?=E9=99=A4=E5=B9=B6=E5=8D=95=E7=8B=AC=E5=A4=84=E7=90=86=20skills?= =?UTF-8?q?/bundled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在复制 core/templates 到 dist/templates 时排除 skills/bundled 目录 - 将 skills/bundled 资源单独复制到 dist/bundled 目录 - 更新 getBundledSkillsRoot 方法以匹配新的 bundled 资源路径 - 添加路径存在性检查,确保模板目录存在 - 优化日志信息,明确排除的目录范围 --- packages/core/src/session.ts | 5 ++++- scripts/copy-bundle-assets.js | 27 +++++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 3460cea5..4483c67f 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -818,12 +818,15 @@ ${agentInstructions} private getBundledSkillsRoot(): string { const extensionRoot = getExtensionRoot(); const sourceRoot = path.join(extensionRoot, "templates", "skills", "bundled"); - const distRoot = path.join(extensionRoot, "dist", "bundled"); // Source check keeps local development/tests on the checked-in templates. if (fs.existsSync(path.join(extensionRoot, "src", "session.ts")) && fs.existsSync(sourceRoot)) { return sourceRoot; } + + // In the published bundle, getExtensionRoot() resolves to dist/ and + // bundled skills are copied to dist/bundled/ (not dist/templates/skills/bundled/). + const distRoot = path.join(extensionRoot, "bundled"); return fs.existsSync(distRoot) ? distRoot : sourceRoot; } diff --git a/scripts/copy-bundle-assets.js b/scripts/copy-bundle-assets.js index a5b261aa..c1905c79 100644 --- a/scripts/copy-bundle-assets.js +++ b/scripts/copy-bundle-assets.js @@ -1,5 +1,5 @@ -import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { cpSync, existsSync, mkdirSync, rmSync, statSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -7,31 +7,34 @@ const root = join(__dirname, ".."); const cliRoot = join(root, "packages", "cli"); const distDir = join(cliRoot, "dist"); -// 1. Copy core/templates/ → dist/templates/ -// Core's getExtensionRoot() resolves to dist/ in the bundle (because -// __dirname is dist/chunks/ and ".." goes up to dist/). The runtime code -// loads templates from extensionRoot + "/templates/...", so they must -// exist at dist/templates/. -const templatesSrc = join(root, "packages", "core", "templates"); -const templatesDest = join(distDir, "templates"); - if (!existsSync(distDir)) { mkdirSync(distDir, { recursive: true }); } +const templatesSrc = join(root, "packages", "core", "templates"); +const templatesDest = join(distDir, "templates"); + if (!existsSync(templatesSrc)) { console.error(`Templates directory not found at ${templatesSrc}`); process.exit(1); } +// 1. Copy core/templates/ → dist/templates/, excluding skills/bundled/. +// Bundled skills are copied separately to dist/bundled/ (see step 2) and +// getBundledSkillsRoot() resolves them from there at runtime. rmSync(templatesDest, { recursive: true, force: true }); cpSync(templatesSrc, templatesDest, { recursive: true, dereference: true, + filter: (src) => { + const rel = relative(templatesSrc, src); + // Exclude skills/bundled and everything under it + return !(rel === join("skills", "bundled") || rel.startsWith(join("skills", "bundled") + "/")); + }, }); -console.log("\n✅ Copied core/templates/ → dist/templates/"); +console.log("\n✅ Copied core/templates/ → dist/templates/ (excluding skills/bundled/)"); -// 2. Copy bundled skills to dist/bundled/ (legacy path used by getBundledSkillsRoot fallback) +// 2. Copy bundled skills to dist/bundled/ const bundledSkillsSrc = join(templatesSrc, "skills", "bundled"); const bundledSkillsDest = join(distDir, "bundled"); From 2f33293b2cd6861703f3c8d84362d8458d778956 Mon Sep 17 00:00:00 2001 From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:50:49 +0800 Subject: [PATCH 181/212] =?UTF-8?q?feat(cli):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8F=AF=E6=8F=92=E6=8B=94=E7=8A=B6=E6=80=81=E6=A0=8F=20(statu?= =?UTF-8?q?sline)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在输入框下方渲染由用户配置的状态栏,通过 settings.json#statusline 声明 command/module 两类 provider,无需改动 CLI 源码即可扩展。 - core: 新增 StatusLineProviderConfig/Settings 类型与 normalize/merge 逻辑, resolveSettings 返回 ResolvedStatusLineSettings - cli: 新增 statusline manager、command-provider、module-provider、sanitize; useStatusLine hook;App.tsx 组装 SessionInfo(含 model/thinking/context/ toolUsage);PromptInput.tsx 末尾渲染分段 - docs: 新增 statusline.md / statusline_en.md,configuration 表格补字段 - .deepcode/plugins: 提供 model-info / cwd / git-branch / session-stats / tool-usage 五个示例 mjs - eslint: 为 .deepcode/plugins/**/*.mjs 添加 Node 环境 globals - tests: 新增 statusline.test.ts,覆盖 normalize / merge / manager --- .deepcode/plugins/cwd.mjs | 7 + .deepcode/plugins/git-branch.mjs | 16 ++ .deepcode/plugins/model-info.mjs | 13 + .deepcode/plugins/session-stats.mjs | 22 ++ .deepcode/plugins/tool-usage.mjs | 23 ++ docs/configuration.md | 1 + docs/configuration_en.md | 1 + docs/statusline.md | 149 +++++++++++ docs/statusline_en.md | 149 +++++++++++ eslint.config.mjs | 10 + packages/cli/src/tests/statusline.test.ts | 249 ++++++++++++++++++ packages/cli/src/ui/hooks/index.ts | 2 + packages/cli/src/ui/hooks/useStatusLine.ts | 47 ++++ .../cli/src/ui/statusline/command-provider.ts | 94 +++++++ packages/cli/src/ui/statusline/index.ts | 4 + packages/cli/src/ui/statusline/manager.ts | 169 ++++++++++++ .../cli/src/ui/statusline/module-provider.ts | 91 +++++++ packages/cli/src/ui/statusline/sanitize.ts | 21 ++ packages/cli/src/ui/statusline/types.ts | 39 +++ packages/cli/src/ui/views/App.tsx | 60 +++++ packages/cli/src/ui/views/PromptInput.tsx | 17 ++ packages/core/src/index.ts | 3 + packages/core/src/settings.ts | 146 ++++++++++ 23 files changed, 1333 insertions(+) create mode 100644 .deepcode/plugins/cwd.mjs create mode 100644 .deepcode/plugins/git-branch.mjs create mode 100644 .deepcode/plugins/model-info.mjs create mode 100644 .deepcode/plugins/session-stats.mjs create mode 100644 .deepcode/plugins/tool-usage.mjs create mode 100644 docs/statusline.md create mode 100644 docs/statusline_en.md create mode 100644 packages/cli/src/tests/statusline.test.ts create mode 100644 packages/cli/src/ui/hooks/useStatusLine.ts create mode 100644 packages/cli/src/ui/statusline/command-provider.ts create mode 100644 packages/cli/src/ui/statusline/index.ts create mode 100644 packages/cli/src/ui/statusline/manager.ts create mode 100644 packages/cli/src/ui/statusline/module-provider.ts create mode 100644 packages/cli/src/ui/statusline/sanitize.ts create mode 100644 packages/cli/src/ui/statusline/types.ts diff --git a/.deepcode/plugins/cwd.mjs b/.deepcode/plugins/cwd.mjs new file mode 100644 index 00000000..d36099b4 --- /dev/null +++ b/.deepcode/plugins/cwd.mjs @@ -0,0 +1,7 @@ +export default function cwdProvider({ projectRoot }) { + const cwd = process.cwd() || projectRoot || ""; + if (!cwd) return ""; + const home = process.env.HOME || process.env.USERPROFILE || ""; + const display = home && cwd.startsWith(home) ? "~" + cwd.slice(home.length) : cwd; + return display; +} diff --git a/.deepcode/plugins/git-branch.mjs b/.deepcode/plugins/git-branch.mjs new file mode 100644 index 00000000..b5096073 --- /dev/null +++ b/.deepcode/plugins/git-branch.mjs @@ -0,0 +1,16 @@ +import { execFileSync } from "node:child_process"; + +export default function gitBranchProvider({ projectRoot }) { + try { + const out = execFileSync("git", ["branch", "--show-current"], { + cwd: projectRoot || process.cwd(), + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 1500, + }).trim(); + if (!out) return ""; + return `git:${out}`; + } catch { + return ""; + } +} diff --git a/.deepcode/plugins/model-info.mjs b/.deepcode/plugins/model-info.mjs new file mode 100644 index 00000000..71a4ec09 --- /dev/null +++ b/.deepcode/plugins/model-info.mjs @@ -0,0 +1,13 @@ +export default function modelInfoProvider({ session }) { + if (!session) return ""; + const parts = []; + if (session.model) { + parts.push(session.model); + } + if (session.thinkingEnabled && session.reasoningEffort) { + parts.push(`thinking:${session.reasoningEffort}`); + } else if (session.thinkingEnabled) { + parts.push("thinking"); + } + return parts.join(" "); +} diff --git a/.deepcode/plugins/session-stats.mjs b/.deepcode/plugins/session-stats.mjs new file mode 100644 index 00000000..79aca645 --- /dev/null +++ b/.deepcode/plugins/session-stats.mjs @@ -0,0 +1,22 @@ +function formatTokens(n) { + if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k"; + return String(n); +} + +export default function sessionStatsProvider({ session }) { + if (!session || !session.activeSessionId) { + return "no session"; + } + const parts = []; + parts.push(`msgs:${session.messageCount}`); + if (session.requestCount > 0) { + parts.push(`reqs:${session.requestCount}`); + } + if (session.activeTokens > 0 && session.maxContextTokens > 0) { + const pct = Math.round((session.activeTokens / session.maxContextTokens) * 100); + parts.push(`ctx:${formatTokens(session.activeTokens)}/${formatTokens(session.maxContextTokens)} ${pct}%`); + } else if (session.totalTokens > 0) { + parts.push(`tokens:${formatTokens(session.totalTokens)}`); + } + return parts.join(" "); +} diff --git a/.deepcode/plugins/tool-usage.mjs b/.deepcode/plugins/tool-usage.mjs new file mode 100644 index 00000000..212fe45e --- /dev/null +++ b/.deepcode/plugins/tool-usage.mjs @@ -0,0 +1,23 @@ +const TOOL_ORDER = ["bash", "edit", "read", "write", "AskUserQuestion", "UpdatePlan", "WebSearch"]; + +export default function toolUsageProvider({ session }) { + if (!session || !session.activeSessionId) { + return ""; + } + const usage = session.toolUsage; + if (!usage || Object.keys(usage).length === 0) { + return ""; + } + // Sort: preferred order first, then by count desc + const sorted = Object.entries(usage).sort((a, b) => { + const ai = TOOL_ORDER.indexOf(a[0]); + const bi = TOOL_ORDER.indexOf(b[0]); + if (ai !== -1 && bi !== -1) return ai - bi; + if (ai !== -1) return -1; + if (bi !== -1) return 1; + return b[1] - a[1]; + }); + + const shortNames = sorted.slice(0, 6); + return shortNames.map(([name, count]) => `${name}×${count}`).join(" "); +} diff --git a/docs/configuration.md b/docs/configuration.md index 2f198b10..d942dc04 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -37,6 +37,7 @@ Deep Code 使用 `settings.json` 设置文件进行持久化配置,支持两 | `mcpServers` | object | MCP 服务器配置(键为服务名,值为 McpServerConfig 对象) | | `temperature` | number | 模型采样温度,范围 `0` 到 `2` | | `enabledSkills` | object | 按 skill 名称启用或禁用 skill 的配置 | +| `statusline` | object | 状态栏插件配置(参见 [statusline.md](./statusline.md)) | #### `env` 子字段 diff --git a/docs/configuration_en.md b/docs/configuration_en.md index fac8c349..a078c428 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -37,6 +37,7 @@ The following are all the top-level fields supported in `settings.json`, along w | `mcpServers` | object | MCP server configurations (keys are service names, values are McpServerConfig objects) | | `temperature` | number | Sampling temperature for LLM, from `0` to `2` | | `enabledSkills` | object | Per-skill enable/disable map, keyed by skill name | +| `statusline` | object | Status line plugins (see [statusline_en.md](./statusline_en.md)) | #### `env` Sub-fields diff --git a/docs/statusline.md b/docs/statusline.md new file mode 100644 index 00000000..4ab8a11d --- /dev/null +++ b/docs/statusline.md @@ -0,0 +1,149 @@ +# 状态栏插件 + +Deep Code CLI 支持通过插件向终端底部状态栏注入自定义信息(Git 分支、当前时间、token 用量等),无需修改 CLI 源码。状态栏行展示在输入框下方的快捷键提示行下方,所有 provider 的输出会用分隔符拼接后渲染。 + +## 配置 + +在 `~/.deepcode/settings.json`(或项目级 `.deepcode/settings.json`)中添加 `statusline` 字段: + +```jsonc +{ + "statusline": { + "enabled": true, + "refreshMs": 2000, + "separator": " · ", + "providers": [ + { + "type": "command", + "id": "git", + "command": "git branch --show-current", + "color": "cyan" + }, + { + "type": "module", + "id": "tokens", + "path": "./.deepcode/plugins/tokens.mjs", + "color": "yellow" + } + ] + } +} +``` + +### 字段 + +| 字段 | 类型 | 说明 | +| ------------- | --------- | ------------------------------------------------------------------- | +| `enabled` | boolean | 是否启用。省略时,只要存在至少一个 provider 即视为启用 | +| `refreshMs` | number | 拉取间隔毫秒。最小 500,默认 2000 | +| `separator` | string | 多个 provider 输出之间的分隔符,默认 `" · "` | +| `providers` | array | provider 列表,按声明顺序渲染 | + +## Provider 类型 + +### `command` —— 执行外部命令 + +每隔 `refreshMs` 在 shell 中执行一次命令,取 stdout 第一行作为状态栏 segment。 + +| 字段 | 类型 | 必填 | 说明 | +| ----------- | ------- | ---- | ----------------------------------------------------------------- | +| `type` | string | 是 | 固定为 `"command"` | +| `command` | string | 是 | shell 命令(支持管道、重定向等) | +| `id` | string | 否 | 唯一标识。省略时按下标自动生成 | +| `cwd` | string | 否 | 执行目录。相对路径相对于项目根目录,省略时使用项目根目录 | +| `timeoutMs` | number | 否 | 超时毫秒,默认 1500。超时返回空串 | +| `color` | string | 否 | ink 支持的颜色名(如 `"red"`、`"#229ac3"`) | + +示例: + +```json +{ "type": "command", "id": "git", "command": "git status -sb | head -1" } +{ "type": "command", "id": "time", "command": "date +%H:%M" } +{ "type": "command", "id": "node", "command": "node -v", "color": "green" } +``` + +### `module` —— 加载 JS 模块 + +加载本地 JS/MJS 模块,调用其默认导出函数,把返回值作为 segment 文本。 + +| 字段 | 类型 | 必填 | 说明 | +| ----------- | ------- | ---- | ----------------------------------------------------------------------------------- | +| `type` | string | 是 | 固定为 `"module"` | +| `path` | string | 是 | 模块路径。相对路径相对于项目根目录 | +| `id` | string | 否 | 唯一标识 | +| `timeoutMs` | number | 否 | 超时毫秒,默认 2000 | +| `color` | string | 否 | ink 支持的颜色 | + +模块需导出一个 `default` 函数(或具名 `provider`): + +```js +// .deepcode/plugins/tokens.mjs +export default function tokensProvider({ projectRoot, session }) { + // 返回字符串(同步或异步) + if (session?.activeSessionId) { + return `msgs:${session.messageCount} reqs:${session.requestCount} tokens:${session.totalTokens}`; + } + return `tokens: 1.2k`; +} +``` + +函数接收一个对象 `{ projectRoot: string, session: SessionInfo | null }`,返回 `string` 或 `Promise`。 + +`SessionInfo` 结构: + +| 字段 | 类型 | 说明 | +| ----------------- | ------------------ | --------------------------------------------------- | +| `activeSessionId` | `string \| null` | 当前活跃会话的 ID,无会话时为 `null` | +| `messageCount` | `number` | 当前会话中的消息总数 | +| `requestCount` | `number` | 当前会话中的 LLM API 请求次数 | +| `totalTokens` | `number` | 当前会话中消耗的 token 总数 | + +## 安全限制 + +- **module provider 路径必须位于项目根目录或用户家目录之下**,绝对路径在这两个范围之外会被拒绝加载(防止从任意位置执行代码)。 +- 单个 segment 文本被自动: + - 取第一个非空行 + - 去除 ANSI 转义序列 + - 折叠空白字符 + - 截断到 40 个字符(超出加 `…`) +- command provider 的 stdout 最多读取 4 KB。 +- 任何 provider 抛错、超时、或返回空字符串,**只跳过该 segment**,不影响其它 provider。 + +## 行为 + +- 启动 CLI 后立即触发一次拉取,之后按 `refreshMs` 周期刷新。 +- 用户级与项目级配置的 `providers` 数组会**合并**(用户先、项目后);其他字段以项目级为优先。 +- 状态栏行在任何场景下都显示(包括 busy、permission prompt 等),不影响 busy 提示。 +- 修改配置文件后需重启 CLI 生效(不会热加载)。 + +## 完整示例 + +```json +{ + "statusline": { + "enabled": true, + "refreshMs": 3000, + "providers": [ + { + "type": "command", + "id": "branch", + "command": "git branch --show-current", + "color": "cyan" + }, + { + "type": "command", + "id": "dirty", + "command": "git status --porcelain | wc -l | xargs -I{} echo '{} files changed'", + "color": "yellow" + }, + { + "type": "module", + "id": "ts-errors", + "path": "./.deepcode/plugins/ts-errors.mjs", + "color": "red", + "timeoutMs": 5000 + } + ] + } +} +``` diff --git a/docs/statusline_en.md b/docs/statusline_en.md new file mode 100644 index 00000000..bd14d91a --- /dev/null +++ b/docs/statusline_en.md @@ -0,0 +1,149 @@ +# Status Line Plugins + +Deep Code CLI lets you inject custom information into the status line at the bottom of the terminal (Git branch, current time, token usage, etc.) through plugins, without modifying the CLI source. The status line renders below the keyboard hint line under the prompt input, and all provider outputs are concatenated with a separator. + +## Configuration + +Add a `statusline` field to `~/.deepcode/settings.json` (or the project-level `.deepcode/settings.json`): + +```jsonc +{ + "statusline": { + "enabled": true, + "refreshMs": 2000, + "separator": " · ", + "providers": [ + { + "type": "command", + "id": "git", + "command": "git branch --show-current", + "color": "cyan" + }, + { + "type": "module", + "id": "tokens", + "path": "./.deepcode/plugins/tokens.mjs", + "color": "yellow" + } + ] + } +} +``` + +### Fields + +| Field | Type | Description | +| ------------- | --------- | ---------------------------------------------------------------------------- | +| `enabled` | boolean | Whether the status line is enabled. If omitted, defaults to true when at least one provider is configured. | +| `refreshMs` | number | Refresh interval in milliseconds. Minimum 500, default 2000. | +| `separator` | string | Separator between provider outputs. Default `" · "`. | +| `providers` | array | List of providers, rendered in declaration order. | + +## Provider Types + +### `command` — Run an External Command + +Executes a shell command every `refreshMs` and uses the first line of stdout as the status segment. + +| Field | Type | Required | Description | +| ----------- | ------- | -------- | ------------------------------------------------------------------------ | +| `type` | string | Yes | Must be `"command"`. | +| `command` | string | Yes | Shell command (supports pipes, redirection, etc.). | +| `id` | string | No | Unique identifier. Auto-generated from index if omitted. | +| `cwd` | string | No | Working directory. Relative paths resolved against the project root. | +| `timeoutMs` | number | No | Timeout in milliseconds. Default 1500. Empty string on timeout. | +| `color` | string | No | Ink-supported color (e.g. `"red"`, `"#229ac3"`). | + +Examples: + +```json +{ "type": "command", "id": "git", "command": "git status -sb | head -1" } +{ "type": "command", "id": "time", "command": "date +%H:%M" } +{ "type": "command", "id": "node", "command": "node -v", "color": "green" } +``` + +### `module` — Load a JS Module + +Loads a local JS/MJS module and calls its default-exported function. The return value becomes the segment text. + +| Field | Type | Required | Description | +| ----------- | ------- | -------- | ------------------------------------------------------------------------------------ | +| `type` | string | Yes | Must be `"module"`. | +| `path` | string | Yes | Module path. Relative paths resolved against the project root. | +| `id` | string | No | Unique identifier. | +| `timeoutMs` | number | No | Timeout in milliseconds. Default 2000. | +| `color` | string | No | Ink-supported color. | + +The module must export a `default` function (or a named `provider`): + +```js +// .deepcode/plugins/tokens.mjs +export default function tokensProvider({ projectRoot, session }) { + // Return a string (sync or async). + if (session?.activeSessionId) { + return `msgs:${session.messageCount} reqs:${session.requestCount} tokens:${session.totalTokens}`; + } + return `tokens: 1.2k`; +} +``` + +The function receives `{ projectRoot: string, session: SessionInfo | null }` and returns `string` or `Promise`. + +`SessionInfo` shape: + +| Field | Type | Description | +| ----------------- | ------------------- | ---------------------------------------------------------- | +| `activeSessionId` | `string \| null` | ID of the currently active session, or `null` if none. | +| `messageCount` | `number` | Total messages in the active session. | +| `requestCount` | `number` | Total LLM API requests made in the active session. | +| `totalTokens` | `number` | Total tokens consumed in the active session. | + +## Safety Constraints + +- **Module provider paths must reside within the project root or the user's home directory**; absolute paths outside both are rejected (to prevent loading arbitrary code). +- Each segment's text is automatically: + - Reduced to the first non-empty line + - Stripped of ANSI escape sequences + - Whitespace-collapsed + - Truncated to 40 characters (with `…` for overflow) +- Command provider stdout is capped at 4 KB. +- If any provider throws, times out, or returns an empty string, **only that segment is skipped**; the rest are unaffected. + +## Behavior + +- The first refresh fires immediately after CLI startup, then on the configured interval. +- The `providers` arrays from user-level and project-level configs are **merged** (user first, project second); other fields prefer the project-level value. +- The status line is shown in every state (including busy and permission prompts) without interfering with busy indicators. +- Changes to config require a CLI restart (no hot reload). + +## Full Example + +```json +{ + "statusline": { + "enabled": true, + "refreshMs": 3000, + "providers": [ + { + "type": "command", + "id": "branch", + "command": "git branch --show-current", + "color": "cyan" + }, + { + "type": "command", + "id": "dirty", + "command": "git status --porcelain | wc -l | xargs -I{} echo '{} files changed'", + "color": "yellow" + }, + { + "type": "module", + "id": "ts-errors", + "path": "./.deepcode/plugins/ts-errors.mjs", + "color": "red", + "timeoutMs": 5000 + } + ] + } +} +``` diff --git a/eslint.config.mjs b/eslint.config.mjs index b9ec5dc1..abeb8390 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -65,6 +65,16 @@ export default tseslint.config( }, }, }, + // Statusline plugins: Node.js environment + { + files: [".deepcode/plugins/**/*.mjs", ".deepcode/plugins/**/*.js"], + languageOptions: { + globals: { + process: "readonly", + console: "readonly", + }, + }, + }, // Browser resources: VSCode webview scripts { files: ["packages/*/resources/**/*.js"], diff --git a/packages/cli/src/tests/statusline.test.ts b/packages/cli/src/tests/statusline.test.ts new file mode 100644 index 00000000..fffc15a1 --- /dev/null +++ b/packages/cli/src/tests/statusline.test.ts @@ -0,0 +1,249 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { sanitizeStatusText, STATUS_SEGMENT_MAX_LENGTH } from "../ui/statusline/sanitize"; +import { validateModulePath, loadModuleProvider } from "../ui/statusline/module-provider"; +import { createCommandStatusProvider } from "../ui/statusline/command-provider"; +import { StatusLineManager } from "../ui/statusline/manager"; +import { resolveSettings } from "@vegamo/deepcode-core"; +import type { ResolvedStatusLineSettings } from "@vegamo/deepcode-core"; + +test("sanitizeStatusText returns empty for null/undefined/empty", () => { + assert.equal(sanitizeStatusText(undefined), ""); + assert.equal(sanitizeStatusText(null), ""); + assert.equal(sanitizeStatusText(""), ""); +}); + +test("sanitizeStatusText keeps first non-empty line and strips ANSI", () => { + assert.equal(sanitizeStatusText("\n\nfirst\nsecond"), "first"); + assert.equal(sanitizeStatusText("red text"), "red text"); + assert.equal(sanitizeStatusText("multiple spaces\t\there"), "multiple spaces here"); +}); + +test("sanitizeStatusText truncates to max length with ellipsis", () => { + const long = "x".repeat(STATUS_SEGMENT_MAX_LENGTH + 20); + const result = sanitizeStatusText(long); + assert.equal(result.length, STATUS_SEGMENT_MAX_LENGTH); + assert.ok(result.endsWith("…")); +}); + +test("sanitizeStatusText respects custom max length", () => { + assert.equal(sanitizeStatusText("hello world", 5), "hell…"); + assert.equal(sanitizeStatusText("hi", 5), "hi"); +}); + +test("validateModulePath accepts paths under project root", () => { + const projectRoot = path.resolve(os.tmpdir(), "deepcode-test-project"); + const inside = path.join(projectRoot, "plugins", "status.js"); + const result = validateModulePath(inside, projectRoot); + assert.equal(result, path.normalize(inside)); +}); + +test("validateModulePath accepts relative paths resolved under project root", () => { + const projectRoot = path.resolve(os.tmpdir(), "deepcode-test-project"); + const result = validateModulePath("plugins/status.js", projectRoot); + assert.equal(result, path.normalize(path.join(projectRoot, "plugins", "status.js"))); +}); + +test("validateModulePath rejects paths outside project root and home", () => { + const projectRoot = path.resolve(os.tmpdir(), "deepcode-isolated-test"); + // Use a path guaranteed to be outside both projectRoot and HOME. + const outside = path.resolve("/totally-not-in-any-allowed-base/status.js"); + const result = validateModulePath(outside, projectRoot); + assert.equal(result, null); +}); + +test("resolveSettings produces a default statusline with no providers", () => { + const resolved = resolveSettings({}, { model: "default-model", baseURL: "https://default.example.com" }, {}); + assert.equal(resolved.statusline.enabled, false); + assert.equal(resolved.statusline.refreshMs, 2000); + assert.deepEqual(resolved.statusline.providers, []); +}); + +test("resolveSettings normalizes statusline providers and filters invalid entries", () => { + const resolved = resolveSettings( + { + statusline: { + enabled: true, + refreshMs: 3000, + providers: [ + { type: "command", id: "git", command: "git status -sb" }, + { type: "command", command: "" } as never, // invalid: empty command + { type: "module", path: "./plugins/x.js" }, + { type: "module" } as never, // invalid: missing path + { type: "unknown" } as never, // invalid: bad type + ], + }, + }, + { model: "default-model", baseURL: "https://default.example.com" }, + {} + ); + assert.equal(resolved.statusline.enabled, true); + assert.equal(resolved.statusline.refreshMs, 3000); + assert.equal(resolved.statusline.providers.length, 2); + assert.equal(resolved.statusline.providers[0]?.type, "command"); + assert.equal(resolved.statusline.providers[1]?.type, "module"); +}); + +test("resolveSettings clamps refreshMs to minimum and ignores invalid values", () => { + const tooSmall = resolveSettings({ statusline: { refreshMs: 100 } }, { model: "m", baseURL: "https://e" }, {}); + assert.equal(tooSmall.statusline.refreshMs, 2000); // falls back to default +}); + +test("createCommandStatusProvider returns stdout from short commands", async () => { + const provider = createCommandStatusProvider( + { type: "command", command: process.platform === "win32" ? "echo hello" : "printf hello" }, + process.cwd(), + "test-cmd" + ); + const ac = new AbortController(); + const result = await provider.fetch({ projectRoot: process.cwd(), signal: ac.signal }); + assert.ok(result.includes("hello")); +}); + +test("createCommandStatusProvider times out long-running commands", async () => { + const sleepCmd = process.platform === "win32" ? "ping -n 5 127.0.0.1 > nul" : "sleep 3"; + const provider = createCommandStatusProvider( + { type: "command", command: sleepCmd, timeoutMs: 200 }, + process.cwd(), + "slow" + ); + const ac = new AbortController(); + const start = Date.now(); + const result = await provider.fetch({ projectRoot: process.cwd(), signal: ac.signal }); + const elapsed = Date.now() - start; + assert.ok(elapsed < 1500, `expected timeout within ~1.5s, got ${elapsed}ms`); + assert.equal(result, ""); +}); + +test("createCommandStatusProvider returns empty on non-existent command", async () => { + const provider = createCommandStatusProvider( + { type: "command", command: "this-command-definitely-does-not-exist-xyz-abc-12345" }, + process.cwd(), + "missing" + ); + const ac = new AbortController(); + const result = await provider.fetch({ projectRoot: process.cwd(), signal: ac.signal }); + // Either empty (failure) or shell error message — both fine, just must not hang/throw. + assert.equal(typeof result, "string"); +}); + +test("loadModuleProvider returns null when the path does not exist", async () => { + const provider = await loadModuleProvider( + path.join(os.tmpdir(), "does-not-exist-xyz.mjs"), + undefined, + "missing", + 1000 + ); + assert.equal(provider, null); +}); + +test("loadModuleProvider isolates errors thrown by the user function", async () => { + // Create a temporary module that throws. + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-statusline-")); + const modPath = path.join(dir, "bad.mjs"); + fs.writeFileSync(modPath, "export default () => { throw new Error('boom'); }", "utf8"); + try { + const provider = await loadModuleProvider(modPath, undefined, "bad", 1000); + assert.ok(provider, "provider should load even if its fn throws on invocation"); + const ac = new AbortController(); + await assert.rejects(provider!.fetch({ projectRoot: process.cwd(), signal: ac.signal })); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test("loadModuleProvider succeeds for a well-formed module", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-statusline-")); + const modPath = path.join(dir, "good.mjs"); + fs.writeFileSync(modPath, "export default ({ projectRoot }) => `root=${projectRoot}`;", "utf8"); + try { + const provider = await loadModuleProvider(modPath, "yellow", "good", 1000); + assert.ok(provider); + assert.equal(provider!.color, "yellow"); + const ac = new AbortController(); + const result = await provider!.fetch({ projectRoot: "/some/root", signal: ac.signal }); + assert.equal(result, "root=/some/root"); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +test("StatusLineManager emits segments after fetch and stops cleanly", async () => { + const config: ResolvedStatusLineSettings = { + enabled: true, + refreshMs: 60_000, + separator: " · ", + providers: [ + { + type: "command", + id: "echo", + command: process.platform === "win32" ? "echo hello" : "printf hello", + }, + ], + }; + const manager = new StatusLineManager(); + const updates: Array> = []; + const unsub = manager.subscribe((segments) => updates.push(segments.map((s) => ({ id: s.id, text: s.text })))); + await manager.start(config, process.cwd()); + + // Wait for the initial fetch to settle. + await new Promise((resolve) => setTimeout(resolve, 400)); + + unsub(); + manager.stop(); + + const populated = updates.find((u) => u.length > 0 && u[0]?.text.includes("hello")); + assert.ok(populated, `expected an update with 'hello' segment; got ${JSON.stringify(updates)}`); +}); + +test("StatusLineManager skips fetch when disabled", async () => { + const config: ResolvedStatusLineSettings = { + enabled: false, + refreshMs: 60_000, + separator: " · ", + providers: [{ type: "command", command: "echo whatever" }], + }; + const manager = new StatusLineManager(); + const updates: Array<{ id: string; text: string }[]> = []; + manager.subscribe((segs) => updates.push(segs.map((s) => ({ id: s.id, text: s.text })))); + await manager.start(config, process.cwd()); + await new Promise((resolve) => setTimeout(resolve, 100)); + manager.stop(); + assert.equal(updates.length, 0); +}); + +test("StatusLineManager isolates a failing provider from succeeding ones", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-statusline-")); + const badMod = path.join(dir, "bad.mjs"); + const goodMod = path.join(dir, "good.mjs"); + fs.writeFileSync(badMod, "export default () => { throw new Error('boom'); }", "utf8"); + fs.writeFileSync(goodMod, "export default () => 'ok';", "utf8"); + + try { + const config: ResolvedStatusLineSettings = { + enabled: true, + refreshMs: 60_000, + separator: " · ", + providers: [ + { type: "module", id: "bad", path: badMod }, + { type: "module", id: "good", path: goodMod }, + ], + }; + const manager = new StatusLineManager(); + let lastSegments: Array<{ id: string; text: string }> = []; + manager.subscribe((segs) => { + lastSegments = segs.map((s) => ({ id: s.id, text: s.text })); + }); + await manager.start(config, dir); + await new Promise((resolve) => setTimeout(resolve, 400)); + manager.stop(); + assert.equal(lastSegments.length, 1); + assert.equal(lastSegments[0]?.id, "good"); + assert.equal(lastSegments[0]?.text, "ok"); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/cli/src/ui/hooks/index.ts b/packages/cli/src/ui/hooks/index.ts index 226a6e98..4e6bda52 100644 --- a/packages/cli/src/ui/hooks/index.ts +++ b/packages/cli/src/ui/hooks/index.ts @@ -17,3 +17,5 @@ export type { PasteRegion, PasteHandlingState, PasteHandlingActions } from "./us export { useHistoryNavigation } from "./useHistoryNavigation"; export type { HistoryNavigationState, HistoryNavigationActions } from "./useHistoryNavigation"; + +export { useStatusLine } from "./useStatusLine"; diff --git a/packages/cli/src/ui/hooks/useStatusLine.ts b/packages/cli/src/ui/hooks/useStatusLine.ts new file mode 100644 index 00000000..f84534ec --- /dev/null +++ b/packages/cli/src/ui/hooks/useStatusLine.ts @@ -0,0 +1,47 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import type { ResolvedStatusLineSettings } from "@vegamo/deepcode-core"; +import { StatusLineManager } from "../statusline"; +import type { SessionInfo, StatusSegment } from "../statusline"; + +/** + * Manages a StatusLineManager lifecycle and returns the current segments. + * Starts polling when the config is enabled, stops on unmount or config change. + */ +export function useStatusLine( + config: ResolvedStatusLineSettings, + projectRoot: string, + getSessionInfo?: () => SessionInfo | null +): StatusSegment[] { + const [segments, setSegments] = useState([]); + const managerRef = useRef(null); + const getSessionInfoRef = useRef(getSessionInfo); + getSessionInfoRef.current = getSessionInfo; + + const configKey = useMemo( + () => + JSON.stringify({ + enabled: config.enabled, + refreshMs: config.refreshMs, + separator: config.separator, + providers: config.providers, + }), + [config] + ); + + useEffect(() => { + const manager = new StatusLineManager(); + managerRef.current = manager; + + const unsub = manager.subscribe(setSegments); + void manager.start(config, projectRoot, () => (getSessionInfoRef.current ? getSessionInfoRef.current() : null)); + + return () => { + unsub(); + manager.stop(); + managerRef.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- config tracked via configKey + }, [configKey, projectRoot]); + + return segments; +} diff --git a/packages/cli/src/ui/statusline/command-provider.ts b/packages/cli/src/ui/statusline/command-provider.ts new file mode 100644 index 00000000..fb6327d0 --- /dev/null +++ b/packages/cli/src/ui/statusline/command-provider.ts @@ -0,0 +1,94 @@ +import { spawn } from "child_process"; +import * as path from "path"; +import type { StatusLineProviderConfig } from "@vegamo/deepcode-core"; +import type { StatusProvider, StatusProviderContext } from "./types"; + +const DEFAULT_TIMEOUT_MS = 1500; +const MIN_TIMEOUT_MS = 100; +const MAX_OUTPUT_BYTES = 4096; + +function resolveTimeout(value: number | undefined): number { + if (typeof value !== "number" || !Number.isFinite(value) || value < MIN_TIMEOUT_MS) { + return DEFAULT_TIMEOUT_MS; + } + return Math.floor(value); +} + +function resolveCwd(configCwd: string | undefined, projectRoot: string): string { + if (!configCwd) { + return projectRoot; + } + return path.isAbsolute(configCwd) ? configCwd : path.resolve(projectRoot, configCwd); +} + +export function createCommandStatusProvider( + config: Extract, + projectRoot: string, + id: string +): StatusProvider { + const timeoutMs = resolveTimeout(config.timeoutMs); + const cwd = resolveCwd(config.cwd, projectRoot); + + return { + id, + color: config.color, + maxLength: config.maxLength, + fetch: ({ signal }: StatusProviderContext) => + new Promise((resolve) => { + if (signal.aborted) { + resolve(""); + return; + } + const isWindows = process.platform === "win32"; + const child = spawn(config.command, { + cwd, + shell: isWindows ? true : "/bin/sh", + windowsHide: true, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stdoutBytes = 0; + let settled = false; + const finish = (value: string): void => { + if (settled) { + return; + } + settled = true; + cleanup(); + if (!child.killed) { + child.kill(); + } + resolve(value); + }; + + const onAbort = (): void => finish(""); + signal.addEventListener("abort", onAbort, { once: true }); + + const timer = setTimeout(() => finish(""), timeoutMs); + + const cleanup = (): void => { + clearTimeout(timer); + signal.removeEventListener("abort", onAbort); + }; + + child.stdout?.on("data", (chunk: Buffer | string) => { + if (settled) { + return; + } + const text = typeof chunk === "string" ? chunk : chunk.toString("utf8"); + if (stdoutBytes >= MAX_OUTPUT_BYTES) { + return; + } + const remaining = MAX_OUTPUT_BYTES - stdoutBytes; + const slice = text.length > remaining ? text.slice(0, remaining) : text; + stdout += slice; + stdoutBytes += slice.length; + }); + // Drain stderr to avoid blocking, but ignore content. + child.stderr?.on("data", () => undefined); + child.on("error", () => finish("")); + child.on("close", () => finish(stdout)); + }), + }; +} diff --git a/packages/cli/src/ui/statusline/index.ts b/packages/cli/src/ui/statusline/index.ts new file mode 100644 index 00000000..1299c8ec --- /dev/null +++ b/packages/cli/src/ui/statusline/index.ts @@ -0,0 +1,4 @@ +export { StatusLineManager } from "./manager"; +export { sanitizeStatusText, STATUS_SEGMENT_MAX_LENGTH } from "./sanitize"; +export { validateModulePath } from "./module-provider"; +export type { StatusSegment, StatusProvider, StatusProviderContext, SessionInfo } from "./types"; diff --git a/packages/cli/src/ui/statusline/manager.ts b/packages/cli/src/ui/statusline/manager.ts new file mode 100644 index 00000000..4ebd8888 --- /dev/null +++ b/packages/cli/src/ui/statusline/manager.ts @@ -0,0 +1,169 @@ +import type { ResolvedStatusLineSettings, StatusLineProviderConfig } from "@vegamo/deepcode-core"; +import { sanitizeStatusText } from "./sanitize"; +import { createCommandStatusProvider } from "./command-provider"; +import { loadModuleProvider, validateModulePath } from "./module-provider"; +import type { SessionInfo, StatusProvider, StatusSegment } from "./types"; + +type SegmentsListener = (segments: StatusSegment[]) => void; + +function segmentsEqual(a: StatusSegment[], b: StatusSegment[]): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (a[i]?.id !== b[i]?.id || a[i]?.text !== b[i]?.text || a[i]?.color !== b[i]?.color) { + return false; + } + } + return true; +} + +export class StatusLineManager { + private providers: StatusProvider[] = []; + private ac: AbortController | null = null; + private timer: ReturnType | null = null; + private subscribers = new Set(); + private segments: StatusSegment[] = []; + private running = false; + private projectRoot = ""; + private getSessionInfo: (() => SessionInfo | null) | undefined; + + get currentSegments(): StatusSegment[] { + return this.segments; + } + + subscribe(fn: SegmentsListener): () => void { + this.subscribers.add(fn); + return () => { + this.subscribers.delete(fn); + }; + } + + private emit(segments: StatusSegment[]): void { + if (segmentsEqual(this.segments, segments)) { + return; + } + this.segments = segments; + for (const fn of this.subscribers) { + try { + fn(segments); + } catch { + // ignore subscriber errors + } + } + } + + async start( + config: ResolvedStatusLineSettings, + projectRoot: string, + getSessionInfo?: () => SessionInfo | null + ): Promise { + if (this.running) { + this.stop(); + } + if (!config.enabled || config.providers.length === 0) { + return; + } + + this.projectRoot = projectRoot; + this.getSessionInfo = getSessionInfo; + const { providers, refreshMs } = config; + this.ac = new AbortController(); + const { signal } = this.ac; + + // Build providers + const built: StatusProvider[] = []; + let nextId = 0; + for (const entry of providers) { + const providerId = entry.id || `${entry.type}-${nextId}`; + const provider = await this.buildProvider(entry, projectRoot, providerId); + if (provider) { + built.push(provider); + } + nextId += 1; + } + + if (built.length === 0) { + return; + } + + this.providers = built; + this.running = true; + + // Fetch immediately, then on interval. + void this.fetchAll(); + this.timer = setInterval(() => { + if (signal.aborted) { + return; + } + void this.fetchAll(); + }, refreshMs); + } + + stop(): void { + this.running = false; + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + if (this.ac) { + this.ac.abort(); + this.ac = null; + } + for (const provider of this.providers) { + provider.dispose?.(); + } + this.providers = []; + this.getSessionInfo = undefined; + } + + private async buildProvider( + config: StatusLineProviderConfig, + projectRoot: string, + providerId: string + ): Promise { + if (config.type === "command") { + return createCommandStatusProvider(config, projectRoot, providerId); + } + if (config.type === "module") { + const resolvedPath = validateModulePath(config.path, projectRoot); + if (!resolvedPath) { + return null; + } + return loadModuleProvider(resolvedPath, config.color, providerId, config.timeoutMs, config.maxLength); + } + return null; + } + + private async fetchAll(): Promise { + if (!this.ac || this.ac.signal.aborted) { + return; + } + + const results = await Promise.all( + this.providers.map(async (provider) => { + try { + const text = await provider.fetch({ + projectRoot: this.projectRoot, + signal: this.ac!.signal, + getSessionInfo: this.getSessionInfo, + }); + const sanitized = sanitizeStatusText(text, provider.maxLength); + if (!sanitized) { + return null; + } + const segment: StatusSegment = { id: provider.id, text: sanitized }; + if (provider.color) { + segment.color = provider.color; + } + return segment; + } catch { + return null; + } + }) + ); + + const segments = results.filter((s): s is StatusSegment => s !== null); + this.emit(segments); + } +} diff --git a/packages/cli/src/ui/statusline/module-provider.ts b/packages/cli/src/ui/statusline/module-provider.ts new file mode 100644 index 00000000..0222bb6c --- /dev/null +++ b/packages/cli/src/ui/statusline/module-provider.ts @@ -0,0 +1,91 @@ +import * as path from "path"; +import type { StatusProvider, StatusProviderContext } from "./types"; + +const DEFAULT_TIMEOUT_MS = 2000; + +/** + * Validate that the module path is within the allowed base directory. + * Only paths under or relative to the project root or home directory are allowed. + */ +export function validateModulePath(modulePath: string, projectRoot: string): string | null { + // Resolve relative to project root first. + const resolved = path.isAbsolute(modulePath) ? modulePath : path.resolve(projectRoot, modulePath); + const normalized = path.normalize(resolved); + + const homeDir = process.env.HOME || process.env.USERPROFILE || ""; + const allowedBases = [projectRoot]; + if (homeDir) { + allowedBases.push(homeDir); + } + + for (const base of allowedBases) { + const normalizedBase = path.normalize(base); + // Check if the resolved path is under the allowed base. + if (normalized.startsWith(normalizedBase + path.sep) || normalized === normalizedBase) { + return normalized; + } + } + return null; +} + +export async function loadModuleProvider( + resolvedPath: string, + color: string | undefined, + id: string, + timeoutMs: number | undefined, + maxLength?: number +): Promise { + try { + const timeout = + typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 100 + ? Math.floor(timeoutMs) + : DEFAULT_TIMEOUT_MS; + + let mod: unknown; + try { + mod = await import(resolvedPath); + } catch { + // Try with file:// protocol + const fileUrl = path.isAbsolute(resolvedPath) ? `file://${resolvedPath}` : resolvedPath; + mod = await import(fileUrl); + } + + const providerFn = (mod as Record).default ?? (mod as Record).provider; + if (typeof providerFn !== "function") { + return null; + } + + return { + id, + color, + maxLength, + fetch: async (ctx: StatusProviderContext): Promise => { + if (ctx.signal.aborted) { + return ""; + } + const result = await Promise.race([ + Promise.resolve().then(() => + providerFn({ + projectRoot: ctx.projectRoot, + session: ctx.getSessionInfo ? ctx.getSessionInfo() : null, + }) + ), + new Promise((_, reject) => { + const timer = setTimeout(() => reject(new Error("timeout")), timeout); + ctx.signal.addEventListener( + "abort", + () => { + clearTimeout(timer); + reject(new Error("aborted")); + }, + { once: true } + ); + }), + ]); + return typeof result === "string" ? result : ""; + }, + }; + } catch { + return null; + } +} diff --git a/packages/cli/src/ui/statusline/sanitize.ts b/packages/cli/src/ui/statusline/sanitize.ts new file mode 100644 index 00000000..3beb5f7b --- /dev/null +++ b/packages/cli/src/ui/statusline/sanitize.ts @@ -0,0 +1,21 @@ +export const STATUS_SEGMENT_MAX_LENGTH = 40; + +const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g; + +export function sanitizeStatusText(value: unknown, maxLength: number = STATUS_SEGMENT_MAX_LENGTH): string { + if (value === null || value === undefined) { + return ""; + } + const text = typeof value === "string" ? value : String(value); + if (!text) { + return ""; + } + // Take only first non-empty line, strip ANSI escapes, collapse whitespace. + const firstLine = text.split(/\r?\n/).find((line) => line.trim().length > 0) ?? ""; + const stripped = firstLine.replace(ANSI_PATTERN, ""); + const collapsed = stripped.replace(/\s+/g, " ").trim(); + if (collapsed.length <= maxLength) { + return collapsed; + } + return collapsed.slice(0, Math.max(1, maxLength - 1)) + "…"; +} diff --git a/packages/cli/src/ui/statusline/types.ts b/packages/cli/src/ui/statusline/types.ts new file mode 100644 index 00000000..5639138c --- /dev/null +++ b/packages/cli/src/ui/statusline/types.ts @@ -0,0 +1,39 @@ +import type { StatusLineProviderConfig } from "@vegamo/deepcode-core"; + +export type StatusSegment = { + id: string; + text: string; + color?: string; +}; + +export type SessionInfo = { + activeSessionId: string | null; + messageCount: number; + requestCount: number; + totalTokens: number; + activeTokens: number; + maxContextTokens: number; + model: string; + thinkingEnabled: boolean; + reasoningEffort: string; + toolUsage: Record; +}; + +export type StatusProviderContext = { + projectRoot: string; + signal: AbortSignal; + getSessionInfo?: () => SessionInfo | null; +}; + +export type StatusProvider = { + id: string; + color?: string; + maxLength?: number; + fetch: (ctx: StatusProviderContext) => Promise; + dispose?: () => void; +}; + +export type StatusProviderFactory = ( + config: StatusLineProviderConfig, + projectRoot: string +) => Promise; diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index fe1f81cf..e3cf54a2 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -32,6 +32,8 @@ import { renderRawModeMessages, } from "../utils"; import { resolveCurrentSettings, writeModelConfigSelection } from "@vegamo/deepcode-core"; +import { useStatusLine } from "../hooks"; +import type { SessionInfo } from "../statusline"; import { isCollapsedThinking } from "../core/thinking-state"; import { ANSI_CLEAR_SCREEN } from "../constants"; import type { @@ -45,6 +47,7 @@ import type { UserPromptContent, } from "@vegamo/deepcode-core"; import { SessionManager } from "@vegamo/deepcode-core"; +import { getCompactPromptTokenThreshold } from "@vegamo/deepcode-core"; type View = "chat" | "session-list" | "undo" | "mcp-status"; @@ -637,6 +640,61 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl const screenWidth = useMemo(() => columns ?? stdout?.columns ?? 80, [columns, stdout]); const screenHeight = useMemo(() => rows ?? stdout?.rows ?? 24, [rows, stdout]); + const getSessionInfo = useCallback((): SessionInfo | null => { + const activeSessionId = sessionManager.getActiveSessionId(); + const settings = resolveCurrentSettings(projectRoot); + const model = settings.model || ""; + const thinkingEnabled = settings.thinkingEnabled; + const reasoningEffort = settings.reasoningEffort; + const maxContextTokens = getCompactPromptTokenThreshold(model); + if (!activeSessionId) { + return { + activeSessionId: null, + messageCount: 0, + requestCount: 0, + totalTokens: 0, + activeTokens: 0, + maxContextTokens, + model, + thinkingEnabled, + reasoningEffort, + toolUsage: {}, + }; + } + const session = sessionManager.getSession(activeSessionId); + const messages = sessionManager.listSessionMessages(activeSessionId); + const usage = session?.usage; + const totalTokens = + usage && typeof (usage as { total_tokens?: unknown }).total_tokens === "number" + ? ((usage as { total_tokens: number }).total_tokens ?? 0) + : 0; + const requestCount = + usage && typeof (usage as { total_reqs?: unknown }).total_reqs === "number" + ? ((usage as { total_reqs: number }).total_reqs ?? 0) + : 0; + const toolUsage: Record = {}; + for (const msg of messages) { + if (msg.role === "tool" && msg.meta?.function) { + const fn = msg.meta.function as { name?: string }; + if (fn.name) { + toolUsage[fn.name] = (toolUsage[fn.name] || 0) + 1; + } + } + } + return { + activeSessionId, + messageCount: messages.length, + requestCount, + totalTokens, + activeTokens: session?.activeTokens ?? 0, + maxContextTokens, + model, + thinkingEnabled, + reasoningEffort, + toolUsage, + }; + }, [sessionManager, projectRoot]); + const statusLineSegments = useStatusLine(resolvedSettings.statusline, projectRoot, getSessionInfo); const promptHistory = useMemo(() => { return messages .filter((message) => message.role === "user" && typeof message.content === "string") @@ -872,6 +930,8 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl onInterrupt={handleInterrupt} onToggleProcessStdout={handleToggleProcessStdout} placeholder="Type your message..." + statusLineSegments={statusLineSegments} + statusLineSeparator={resolvedSettings.statusline.separator} /> )}
diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index 8124d7aa..9a24abe0 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -64,6 +64,7 @@ import type { ModelConfigSelection, PermissionScope } from "@vegamo/deepcode-cor import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "../components"; import type { SessionEntry, SkillInfo } from "@vegamo/deepcode-core"; import type { UserToolPermission } from "@vegamo/deepcode-core"; +import type { StatusSegment } from "../statusline"; export type PromptSubmission = { text: string; @@ -93,6 +94,8 @@ type Props = { placeholder?: string; runningProcesses?: SessionEntry["processes"]; promptDraft?: PromptDraft | null; + statusLineSegments?: StatusSegment[]; + statusLineSeparator?: string; onSubmit: (submission: PromptSubmission) => void; onModelConfigChange: (selection: ModelConfigSelection) => string | Promise; onRawModeChange?: (mode: string) => void; @@ -123,6 +126,8 @@ export const PromptInput = React.memo(function PromptInput({ placeholder, runningProcesses, promptDraft, + statusLineSegments, + statusLineSeparator, onSubmit, onModelConfigChange, onInterrupt, @@ -839,6 +844,18 @@ export const PromptInput = React.memo(function PromptInput({ {footerText} )} + {statusLineSegments && statusLineSegments.length > 0 && ( + + {statusLineSegments.map((segment, index) => ( + + {index > 0 && {statusLineSeparator ?? " · "}} + + {segment.text} + + + ))} + + )} ); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8ad813a7..832d2444 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -26,6 +26,9 @@ export type { PermissionDefaultMode, McpServerConfig, ReasoningEffort, + StatusLineSettings, + ResolvedStatusLineSettings, + StatusLineProviderConfig, } from "./settings"; // Session diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index c91f0306..f7c9f51f 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -45,6 +45,39 @@ export type PermissionSettings = { export type EnabledSkillsSettings = Record; +export type StatusLineProviderConfig = + | { + type: "command"; + id?: string; + command: string; + cwd?: string; + timeoutMs?: number; + color?: string; + maxLength?: number; + } + | { + type: "module"; + id?: string; + path: string; + timeoutMs?: number; + color?: string; + maxLength?: number; + }; + +export type StatusLineSettings = { + enabled?: boolean; + refreshMs?: number; + separator?: string; + providers?: StatusLineProviderConfig[]; +}; + +export type ResolvedStatusLineSettings = { + enabled: boolean; + refreshMs: number; + separator: string; + providers: StatusLineProviderConfig[]; +}; + export type DeepcodingSettings = { env?: DeepcodingEnv; model?: string; @@ -58,6 +91,7 @@ export type DeepcodingSettings = { mcpServers?: Record; permissions?: PermissionSettings; enabledSkills?: EnabledSkillsSettings; + statusline?: StatusLineSettings; }; export type ResolvedDeepcodingSettings = { @@ -75,6 +109,7 @@ export type ResolvedDeepcodingSettings = { mcpServers?: Record; permissions: Required; enabledSkills: EnabledSkillsSettings; + statusline: ResolvedStatusLineSettings; }; export type ModelConfigSelection = { @@ -216,6 +251,116 @@ function mergeEnabledSkills( }; } +const DEFAULT_STATUSLINE_REFRESH_MS = 2000; +const MIN_STATUSLINE_REFRESH_MS = 500; +const DEFAULT_STATUSLINE_SEPARATOR = " · "; + +function isPlainObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function normalizeStatusLineProvider(value: unknown): StatusLineProviderConfig | null { + if (!isPlainObject(value)) { + return null; + } + const type = value["type"]; + const idRaw = trimString(value["id"]); + const id = idRaw || undefined; + const timeoutRaw = value["timeoutMs"]; + const timeoutMs = + typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw) && timeoutRaw > 0 + ? Math.floor(timeoutRaw) + : undefined; + const colorRaw = trimString(value["color"]); + const color = colorRaw || undefined; + const maxLengthRaw = value["maxLength"]; + const maxLength = + typeof maxLengthRaw === "number" && Number.isFinite(maxLengthRaw) && maxLengthRaw > 0 + ? Math.floor(maxLengthRaw) + : undefined; + + if (type === "command") { + const command = trimString(value["command"]); + if (!command) { + return null; + } + const cwdRaw = trimString(value["cwd"]); + return { + type: "command", + id, + command, + cwd: cwdRaw || undefined, + timeoutMs, + color, + maxLength, + }; + } + if (type === "module") { + const modulePath = trimString(value["path"]); + if (!modulePath) { + return null; + } + return { + type: "module", + id, + path: modulePath, + timeoutMs, + color, + maxLength, + }; + } + return null; +} + +function normalizeStatusLine(value: unknown): StatusLineSettings | null { + if (!isPlainObject(value)) { + return null; + } + const result: StatusLineSettings = {}; + const enabled = parseBoolean(value["enabled"]); + if (enabled !== undefined) { + result.enabled = enabled; + } + const refreshRaw = value["refreshMs"]; + if (typeof refreshRaw === "number" && Number.isFinite(refreshRaw) && refreshRaw >= MIN_STATUSLINE_REFRESH_MS) { + result.refreshMs = Math.floor(refreshRaw); + } + const separator = value["separator"]; + if (typeof separator === "string") { + result.separator = separator; + } + const providers = value["providers"]; + if (Array.isArray(providers)) { + const normalized: StatusLineProviderConfig[] = []; + for (const entry of providers) { + const provider = normalizeStatusLineProvider(entry); + if (provider) { + normalized.push(provider); + } + } + result.providers = normalized; + } + return result; +} + +function mergeStatusLine( + userSettings: DeepcodingSettings | null | undefined, + projectSettings: DeepcodingSettings | null | undefined +): ResolvedStatusLineSettings { + const userConfig = normalizeStatusLine(userSettings?.statusline) ?? {}; + const projectConfig = normalizeStatusLine(projectSettings?.statusline) ?? {}; + const providers = [...(userConfig.providers ?? []), ...(projectConfig.providers ?? [])]; + const enabled = projectConfig.enabled ?? userConfig.enabled ?? providers.length > 0; + const refreshMs = projectConfig.refreshMs ?? userConfig.refreshMs ?? DEFAULT_STATUSLINE_REFRESH_MS; + const separator = projectConfig.separator ?? userConfig.separator ?? DEFAULT_STATUSLINE_SEPARATOR; + return { + enabled, + refreshMs, + separator, + providers, + }; +} + function normalizeEnv(env: DeepcodingSettings["env"]): Record { const result: Record = {}; if (!env) { @@ -393,6 +538,7 @@ export function resolveSettingsSources( mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv), permissions: mergePermissions(userSettings, projectSettings), enabledSkills: mergeEnabledSkills(userSettings, projectSettings), + statusline: mergeStatusLine(userSettings, projectSettings), }; } From f291919df6a2a07b464c7e7b0d602706bf233ada Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 23 Jun 2026 10:52:57 +0800 Subject: [PATCH 182/212] =?UTF-8?q?chore(release):=20=E6=96=B0=E5=A2=9E=20?= =?UTF-8?q?VSCode=20=E6=89=A9=E5=B1=95=E5=8F=91=E5=B8=83=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E5=8F=8A=E6=B5=81=E7=A8=8B=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 scripts/prepare-vscode.js,实现 VSCode 扩展构建发布自动化 - 在 package.json 中新增 prepare:vscode 脚本命令 - 更新 .gitignore,忽略环境变量文件 .env 和 .env.local - 新增 .env.example,说明 VSCE_PAT 环境变量用法 - 在 RELEASE.md 和 RELEASE_en.md 中添加 prepare:vscode 使用说明及发布流程 - 增加发布流程示例,支持单独发布 CLI 和 VSCode 扩展 - 脚本支持版本号参数、dry-run 和 force 分支跳过检查选项 - 业务逻辑包括版本更新、质量检查、测试、构建、发布、Git 提交和打标签 --- .env.example | 4 + .gitignore | 4 + RELEASE.md | 69 +++++++++- RELEASE_en.md | 69 +++++++++- package.json | 1 + scripts/prepare-vscode.js | 260 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 395 insertions(+), 12 deletions(-) create mode 100644 .env.example create mode 100644 scripts/prepare-vscode.js diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..aab21d64 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# VS Code Marketplace publish token +# Generate at: https://dev.azure.com/vegamo/_usersSettings/tokens +# Permission required: Marketplace → Publish +VSCE_PAT= diff --git a/.gitignore b/.gitignore index 37f64ba0..634aea11 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,10 @@ out/ # TypeScript build info files *.tsbuildinfo +# Environment variables +.env +.env.local + # Generated files packages/cli/src/generated/ packages/core/src/generated/ diff --git a/RELEASE.md b/RELEASE.md index 19229167..b6479f30 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,13 +1,14 @@ # 版本发布 -Deep Code 使用两个脚本管理 monorepo 的版本发布流程: +Deep Code 使用三个脚本管理 monorepo 的版本发布流程: | 脚本 | 命令 | 用途 | |------|------|------| | `scripts/version.js` | `npm run release:version` | 升级所有 workspace 包的版本号 + 重新生成 lockfile | -| `scripts/prepare-package.js` | `npm run prepare:package` | 构建 + 质量检查 + 发布到 npm + git commit & tag | +| `scripts/prepare-package.js` | `npm run prepare:package` | 构建 CLI + 质量检查 + 发布到 npm + git commit & tag | +| `scripts/prepare-vscode.js` | `npm run prepare:vscode` | 构建 VSCode 扩展 + 质量检查 + 发布到 VS Code 市场 + git commit & tag | -两者配合使用,先升版本号,再发布。 +发布流程:先升版本号,再分别发布 CLI 和 VSCode 扩展。 --- @@ -172,6 +173,58 @@ npx @vegamo/deepcode-cli --version --- +## prepare:vscode — 构建并发布 VSCode 扩展到市场 + +完成质量检查、构建、发布 VSCode 扩展到 VS Code Marketplace,并自动创建 git commit 和 tag。 + +### 前置条件 + +需要 Azure DevOps Personal Access Token(PAT)用于市场认证: + +1. 访问 https://dev.azure.com/vegamo/_usersSettings/tokens 生成 token +2. 设置环境变量 `VSCE_PAT=` + +### 基本用法 + +```bash +VSCE_PAT= npm run prepare:vscode -- [options] +``` + +### 参数 + +| 参数 | 说明 | +|------|------| +| `` | **必填**,要发布的 semver 版本号 | +| `--dry-run` | 预演模式,不实际执行任何写操作 | +| `--force` | 跳过 main 分支检查,允许从其他分支发布 | + +### 执行流程(7 步) + +| 步骤 | 操作 | 说明 | +|------|------|------| +| 1 | Git 检查 | 工作区必须 clean,必须在 main 分支 | +| 2 | VSCE_PAT 检查 | 环境变量必须已设置 | +| 3 | 更新版本号 | 同时更新 `packages/core`、`packages/cli`、`packages/vscode-ide-companion` 的 version | +| 4 | 质量检查 | `npm run check`(typecheck + eslint + prettier) | +| 5 | 测试 | `npm run test --workspaces` | +| 6 | 构建 | `npm run build:vscode`(core tsc + esbuild 打包扩展 + 拷贝模板 + vsce package) | +| 7 | 发布 | `vsce publish --no-dependencies` 发布到 VS Code Marketplace | + +### 完整示例 + +```bash +# 发布正式版 +VSCE_PAT=xxx npm run prepare:vscode -- 0.1.32 + +# 发布预发布版 +VSCE_PAT=xxx npm run prepare:vscode -- 0.1.32-beta.1 + +# 预演(不实际发布) +npm run prepare:vscode -- 0.1.32 --dry-run +``` + +--- + ## 典型发布流程 一个完整的版本发布通常按以下步骤进行: @@ -190,18 +243,22 @@ git diff git add -A git commit -m "chore(release): v0.1.32" -# 5. 构建 + 质量检查 + 发布 +# 5. 构建 + 质量检查 + 发布 CLI npm run prepare:package -- 0.1.32 -# 6. 推送到 remote +# 6. 发布 VSCode 扩展 +VSCE_PAT=xxx npm run prepare:vscode -- 0.1.32 + +# 7. 推送到 remote git push && git push --tags ``` -也可以简化为两步(`prepare:package` 会自动 commit 和 tag): +也可以简化为三步(`prepare:package` 和 `prepare:vscode` 各自自动 commit 和 tag): ```bash npm run release:version -- patch npm run prepare:package -- 0.1.32 +VSCE_PAT=xxx npm run prepare:vscode -- 0.1.32 git push && git push --tags ``` diff --git a/RELEASE_en.md b/RELEASE_en.md index e6cd7013..2f0fbcca 100644 --- a/RELEASE_en.md +++ b/RELEASE_en.md @@ -1,13 +1,14 @@ # Release -Deep Code uses two scripts to manage version releases in the monorepo: +Deep Code uses three scripts to manage version releases in the monorepo: | Script | Command | Purpose | |--------|---------|---------| | `scripts/version.js` | `npm run release:version` | Bump all workspace package versions + regenerate lockfile | -| `scripts/prepare-package.js` | `npm run prepare:package` | Build + quality checks + publish to npm + git commit & tag | +| `scripts/prepare-package.js` | `npm run prepare:package` | Build CLI + quality checks + publish to npm + git commit & tag | +| `scripts/prepare-vscode.js` | `npm run prepare:vscode` | Build VSCode extension + quality checks + publish to VS Code Marketplace + git commit & tag | -Use them together: bump version first, then publish. +Release flow: bump version first, then publish CLI and VSCode extension separately. --- @@ -172,6 +173,58 @@ npx @vegamo/deepcode-cli --version --- +## prepare:vscode — Build and Publish VSCode Extension to Marketplace + +Runs quality checks, builds, publishes the VSCode extension to the VS Code Marketplace, and automatically creates a git commit with tag. + +### Prerequisites + +Requires an Azure DevOps Personal Access Token (PAT) for marketplace authentication: + +1. Generate a token at https://dev.azure.com/vegamo/_usersSettings/tokens +2. Set the environment variable `VSCE_PAT=` + +### Basic Usage + +```bash +VSCE_PAT= npm run prepare:vscode -- [options] +``` + +### Arguments + +| Argument | Description | +|----------|-------------| +| `` | **Required**. Semver version to publish | +| `--dry-run` | Preview mode, no actual writes | +| `--force` | Skip main branch check, allow publishing from other branches | + +### Execution Flow (7 Steps) + +| Step | Action | Description | +|------|--------|-------------| +| 1 | Git check | Working tree must be clean, must be on main branch | +| 2 | VSCE_PAT check | Environment variable must be set | +| 3 | Update versions | Updates `packages/core`, `packages/cli`, and `packages/vscode-ide-companion` version fields | +| 4 | Quality checks | `npm run check` (typecheck + eslint + prettier) | +| 5 | Tests | `npm run test --workspaces` | +| 6 | Build | `npm run build:vscode` (core tsc + esbuild bundle extension + copy templates + vsce package) | +| 7 | Publish | `vsce publish --no-dependencies` to VS Code Marketplace | + +### Examples + +```bash +# Publish stable release +VSCE_PAT=xxx npm run prepare:vscode -- 0.1.32 + +# Publish pre-release +VSCE_PAT=xxx npm run prepare:vscode -- 0.1.32-beta.1 + +# Dry run (no actual publish) +npm run prepare:vscode -- 0.1.32 --dry-run +``` + +--- + ## Typical Release Flow A complete version release follows these steps: @@ -190,18 +243,22 @@ git diff git add -A git commit -m "chore(release): v0.1.32" -# 5. Build + quality check + publish +# 5. Build + quality check + publish CLI npm run prepare:package -- 0.1.32 -# 6. Push to remote +# 6. Publish VSCode extension +VSCE_PAT=xxx npm run prepare:vscode -- 0.1.32 + +# 7. Push to remote git push && git push --tags ``` -Or simplified to two steps (`prepare:package` auto-commits and tags): +Or simplified to three steps (`prepare:package` and `prepare:vscode` each auto-commit and tag): ```bash npm run release:version -- patch npm run prepare:package -- 0.1.32 +VSCE_PAT=xxx npm run prepare:vscode -- 0.1.32 git push && git push --tags ``` diff --git a/package.json b/package.json index c9a0e723..d4f502b4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "test": "npm run test --workspaces --if-present", "release:version": "node scripts/version.js", "prepare:package": "node scripts/prepare-package.js", + "prepare:vscode": "node scripts/prepare-vscode.js", "prepare": "husky && npm run build && npm run bundle" }, "devDependencies": { diff --git a/scripts/prepare-vscode.js b/scripts/prepare-vscode.js new file mode 100644 index 00000000..a9e0cb83 --- /dev/null +++ b/scripts/prepare-vscode.js @@ -0,0 +1,260 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); + +// Load .env file if VSCE_PAT is not already set +if (!process.env.VSCE_PAT) { + const envPath = join(root, ".env"); + if (existsSync(envPath)) { + const lines = readFileSync(envPath, "utf-8").split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const value = trimmed.slice(eqIdx + 1).trim(); + if (key && !process.env[key]) { + process.env[key] = value; + } + } + } +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function log(msg) { + console.log(msg); +} + +function step(n, total, msg) { + console.log(`\n[${n}/${total}] ${msg}`); +} + +function fail(msg) { + console.error(`\n❌ ${msg}`); + process.exit(1); +} + +function ok(msg) { + console.log(`✅ ${msg}`); +} + +function run(cmd, args, opts = {}) { + const label = opts.label ?? `${cmd} ${args.join(" ")}`; + if (opts.dryRun) { + log(` (dry-run) ${label}`); + return { status: 0, stdout: "" }; + } + const result = spawnSync(cmd, args, { + stdio: opts.stdio ?? "inherit", + cwd: opts.cwd ?? root, + shell: true, + env: { ...process.env, ...opts.env }, + }); + if (result.status !== 0) { + fail(`Command failed: ${label}`); + } + return result; +} + +function readJson(filePath) { + return JSON.parse(readFileSync(filePath, "utf-8")); +} + +function writeJson(filePath, data) { + writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8"); +} + +function isValidSemver(v) { + return /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(v); +} + +// ── Parse args ─────────────────────────────────────────────────────────────── + +const args = process.argv.slice(2); +let version = null; +let dryRun = false; +let force = false; + +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--dry-run") { + dryRun = true; + } else if (arg === "--force") { + force = true; + } else if (!version) { + version = arg; + } else { + fail(`Unknown argument: ${arg}`); + } +} + +if (!version) { + log(` +Usage: node scripts/prepare-vscode.js [options] + +Arguments: + Semver version to publish (e.g. 0.1.32, 0.2.0-beta.1) + +Options: + --dry-run Preview all steps without executing + --force Skip branch check (publish from non-main branch) + +Environment: + VSCE_PAT Required. Azure DevOps Personal Access Token for marketplace auth. + Generate at: https://dev.azure.com/vegamo/_usersSettings/tokens + Can also be set in .env file (auto-loaded). + +Examples: + VSCE_PAT=xxx node scripts/prepare-vscode.js 0.1.32 + node scripts/prepare-vscode.js 0.1.32-beta.1 + node scripts/prepare-vscode.js 0.1.32 --dry-run +`); + process.exit(1); +} + +if (!isValidSemver(version)) { + fail(`Invalid semver version: ${version}`); +} + +const TOTAL_STEPS = 7; + +// ── Banner ─────────────────────────────────────────────────────────────────── + +log("========================================="); +log(` Deep Code VSCode — Publish v${version}`); +log(` dryRun=${dryRun} force=${force}`); +log("========================================="); + +// ── 1. Git checks ──────────────────────────────────────────────────────────── + +step(1, TOTAL_STEPS, "Checking git state..."); + +const gitStatus = spawnSync("git", ["status", "--porcelain"], { + cwd: root, + encoding: "utf-8", + shell: true, +}); +if (gitStatus.stdout.trim()) { + fail("Working tree is not clean. Commit or stash changes first."); +} +ok("Working tree is clean"); + +if (!force) { + const gitBranch = spawnSync("git", ["branch", "--show-current"], { + cwd: root, + encoding: "utf-8", + shell: true, + }); + const branch = gitBranch.stdout.trim(); + if (branch !== "main") { + fail(`Not on main branch (current: ${branch}). Use --force to publish from another branch.`); + } + ok("On main branch"); +} + +// ── 2. VSCE_PAT check ──────────────────────────────────────────────────────── + +step(2, TOTAL_STEPS, "Checking VSCE_PAT..."); + +if (!dryRun) { + if (!process.env.VSCE_PAT) { + fail( + "VSCE_PAT environment variable is not set.\n Generate a Personal Access Token at:\n https://dev.azure.com/vegamo/_usersSettings/tokens\n Then: VSCE_PAT= node scripts/prepare-vscode.js " + ); + } + ok("VSCE_PAT is set"); +} else { + log(" (dry-run) skipping VSCE_PAT check"); +} + +// ── 3. Version bump ────────────────────────────────────────────────────────── + +step(3, TOTAL_STEPS, "Updating package version..."); + +const vscodePkgPath = join(root, "packages", "vscode-ide-companion", "package.json"); + +const vscodePkg = readJson(vscodePkgPath); + +const oldVersion = vscodePkg.version; + +vscodePkg.version = version; + +if (!dryRun) { + writeJson(vscodePkgPath, vscodePkg); + ok(`Updated packages/vscode-ide-companion: ${oldVersion} → ${version}`); +} else { + log(` (dry-run) packages/vscode-ide-companion: ${oldVersion} → ${version}`); +} + +// ── 4. Quality checks ──────────────────────────────────────────────────────── + +step(4, TOTAL_STEPS, "Running quality checks (typecheck + lint + format)..."); + +run("npm", ["run", "check"], { dryRun }); +ok("All checks passed"); + +// ── 5. Tests ────────────────────────────────────────────────────────────────── + +step(5, TOTAL_STEPS, "Running tests..."); + +run("npm", ["run", "test", "--workspaces"], { dryRun }); +ok("All tests passed"); + +// ── 6. Build ────────────────────────────────────────────────────────────────── + +step(6, TOTAL_STEPS, "Building VSCode extension..."); + +run("npm", ["run", "build:vscode"], { dryRun }); +ok("VSCode extension built"); + +// ── 7. Publish to marketplace ───────────────────────────────────────────────── + +step(7, TOTAL_STEPS, "Publishing deepcode-vscode to marketplace..."); + +const vscodeRoot = join(root, "packages", "vscode-ide-companion"); +const vsceArgs = ["vsce", "publish", version, "--no-dependencies"]; +if (dryRun) vsceArgs.splice(2, 0, "--dry-run"); + +run("npx", vsceArgs, { + cwd: vscodeRoot, + env: { VSCE_PAT: process.env.VSCE_PAT }, + label: `npx ${vsceArgs.join(" ")}`, +}); +ok(`Published deepcode-vscode@${version} to marketplace`); + +// ── Git commit + tag ───────────────────────────────────────────────────────── + +if (!dryRun) { + log("\nCreating git commit and tag..."); + run("git", ["add", "packages/vscode-ide-companion/package.json"], { + label: "git add packages/vscode-ide-companion/package.json", + }); + run("git", ["commit", "-m", `chore(release): vscode v${version}`], { + label: `git commit -m "chore(release): vscode v${version}"`, + }); + run("git", ["tag", `vscode-v${version}`], { + label: `git tag vscode-v${version}`, + }); + ok(`Created commit and tag vscode-v${version}`); +} else { + log("\n (dry-run) git add + commit + tag"); +} + +// ── Done ───────────────────────────────────────────────────────────────────── + +console.log("\n========================================="); +console.log(` 🎉 Published deepcode-vscode@${version} successfully!`); +console.log("========================================="); +console.log(` + Verify: + https://marketplace.visualstudio.com/items?itemName=vegamo.deepcode-vscode + + Push to remote: + git push && git push --tags +`); From 01b68053838ef43f6ed1a4d1374865676941f247 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 23 Jun 2026 14:42:59 +0800 Subject: [PATCH 183/212] =?UTF-8?q?chore(vscode-ide-companion):=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20package.json=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 preview 字段,标记为预览版本 - 修正 repository URL 结构,添加目录字段 - 扩展 categories,新增 Chat 分类 - 格式化部分字段列表,使结构更清晰 --- packages/vscode-ide-companion/package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index fd4da3ac..6369f37b 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -7,15 +7,18 @@ "license": "MIT", "type": "commonjs", "main": "./out/extension.js", + "preview": true, "repository": { "type": "git", - "url": "git+https://github.com/lessweb/deepcode-cli.git" + "url": "https://github.com/lessweb/deepcode-cli.git", + "directory": "packages/vscode-ide-companion" }, "engines": { "vscode": "^1.85.0" }, "categories": [ - "AI" + "AI", + "Chat" ], "keywords": [ "deep-code", From bac49377cfd0c74c8ccab4b473c247fb2ead5713 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 23 Jun 2026 15:25:09 +0800 Subject: [PATCH 184/212] docs: update .deepcode/AGENTS.md --- .deepcode/AGENTS.md | 144 ++++++++++++++++++++------------------------ 1 file changed, 65 insertions(+), 79 deletions(-) diff --git a/.deepcode/AGENTS.md b/.deepcode/AGENTS.md index 9d827b0e..a1611307 100644 --- a/.deepcode/AGENTS.md +++ b/.deepcode/AGENTS.md @@ -2,78 +2,61 @@ ## Project Structure & Module Organization +This is an **npm workspaces monorepo**. Packages live under `packages/`. + ``` -src/ -├── cli.tsx # Entry point — parses args (-p, -v), renders Ink App -├── session.ts # SessionManager — LLM loop, compaction, tool orchestration -├── settings.ts # Settings resolution from ~/.deepcode/settings.json -├── prompt.ts # System prompt builder, tool definitions, built-in skills -├── common/ -│ ├── bash-timeout.ts # Bash command timeout enforcement -│ ├── debug-logger.ts # Debug logging for OpenAI API calls -│ ├── error-logger.ts # API error logging -│ ├── file-history.ts # GitFileHistory — checkpoint/undo via Git branches -│ ├── file-utils.ts # File read/write with encoding and diff preview -│ ├── model-capabilities.ts # Model detection and thinking-mode defaults -│ ├── notify.ts # Desktop notification after LLM turn completion -│ ├── openai-client.ts # OpenAI client singleton with keep-alive agent -│ ├── openai-thinking.ts # OpenAI thinking request options builder -│ ├── permissions.ts # Permission scoping, decisions, and tool-call tracking -│ ├── process-tree.ts # Process tree construction and orphan detection -│ ├── shell-utils.ts # Shell path resolution (Git Bash, zsh, bash) -│ ├── state.ts # In-memory file state and snippet tracking -│ ├── telemetry.ts # Usage telemetry collection and reporting -│ ├── update-check.ts # Latest-version check against npm registry -│ └── validate.ts # Tool validation runtime helpers (was runtime.ts) -├── mcp/ -│ ├── mcp-client.ts # MCP client — JSON-RPC communication with MCP servers -│ └── mcp-manager.ts # MCP manager — lifecycle, tool registration, execution, status -├── tools/ -│ ├── executor.ts # ToolExecutor — dispatches tool calls to handlers (7 built-in) -│ ├── bash-handler.ts # Executes shell commands with live stdout streaming -│ ├── read-handler.ts # Reads files, images, PDFs, and notebooks -│ ├── write-handler.ts # Creates/overwrites files -│ ├── edit-handler.ts # Scoped string replacements with snippet tracking -│ ├── update-plan-handler.ts # Updates the task plan progress display -│ ├── web-search-handler.ts # Web search via natural language queries -│ └── ask-user-question-handler.ts # Interactive user prompts with options -├── ui/ -│ ├── components/ # Reusable Ink components (MessageView, DropdownMenu, ModelsDropdown, SkillsDropdown, FileMentionMenu, RawModelDropdown, etc.) -│ ├── contexts/ # React contexts (AppContext, RawModeContext) -│ ├── hooks/ # Custom hooks (cursor, useHistoryNavigation, usePasteHandling, useTerminalInput) -│ ├── views/ # Top-level screen components (App, PromptInput, SessionList, PermissionPrompt, ProcessStdoutView, WelcomeScreen, UndoSelector, etc.) -│ ├── core/ # Core UI logic (file-mentions, slash-commands, prompt-buffer, thinking-state, clipboard, prompt-undo-redo, etc.) -│ ├── utils/ # Shared utility helpers -│ ├── ascii-art.ts # ASCII art banner for welcome screen -│ ├── exit-summary.ts # Session exit summary and cost reporting -│ ├── index.ts # UI module barrel exports -│ └── constants.ts # UI-wide constants -├── tests/ # Test files per source module, plus run-tests.mjs -templates/ -├── tools/ # Tool descriptions fed to the LLM -├── skills/ # Built-in skill definitions (agent-drift-guard, plan-and-execute, karpathy-guidelines) -└── prompts/ # EJS templates (e.g., init_command.md.ejs) -docs/ # User-facing documentation (configuration, MCP, notify, permissions) -resources/ # Static assets (intro screenshots) -dist/ # Bundled CLI output (gitignored) +packages/ +├── core/src/ # LLM session, tool execution, shared utilities +│ ├── common/ # File I/O, permissions, telemetry, OpenAI client, shell utils, etc. +│ ├── tools/ # 7 built-in handlers (bash, read, write, edit, web-search, ask-user-question, update-plan) +│ ├── mcp/ # MCP client & manager (JSON-RPC lifecycle) +│ ├── session.ts # SessionManager — LLM loop, compaction, tool orchestration +│ ├── prompt.ts # System prompt builder & tool definitions +│ └── settings.ts # Settings resolution from ~/.deepcode/settings.json +├── cli/src/ # Terminal UI (Ink/React) +│ ├── cli.tsx # Entry point — parses args (-p, -v), renders AppContainer +│ ├── ui/views/ # Top-level screens (App, PromptInput, SessionList, PermissionPrompt, etc.) +│ ├── ui/components/ # Reusable Ink components (MessageView, DropdownMenu, ModelsDropdown, etc.) +│ ├── ui/core/ # Prompt buffer, slash commands, file mentions, clipboard, undo/redo +│ ├── ui/hooks/ # Custom hooks (cursor, history navigation, paste handling, terminal input) +│ ├── ui/contexts/ # React contexts (AppContext, RawModeContext) +│ └── tests/ # UI-focused tests with run-tests.mjs runner +├── vscode-ide-companion/ # VSCode extension companion +│ └── src/ # extension.ts, provider.ts, utils.ts +docs/ # User-facing documentation (configuration, MCP, notify, permissions) +scripts/ # Build, release, and packaging scripts +dist/ # Bundled CLI output — single-file dist/cli.js (gitignored) +dist/bundled/ # Bundled skills & references shipped with the CLI ``` +Templates for tool descriptions and prompts are at `packages/cli/dist/templates/` (copied during build from `packages/core/templates/`). Built-in skills are under `packages/cli/dist/bundled/`. + ## Build, Test, and Development Commands -| Command | Purpose | +All commands run from the repo root. + +| Command | What it does | |---|---| -| `npm run typecheck` | TypeScript type checking (`tsc --noEmit`) | -| `npm run lint` | ESLint across `src/` | +| `npm run typecheck` | TypeScript type checking across all workspaces | +| `npm run lint` | ESLint across `packages/*/src/**/*.{ts,tsx}` + `scripts/*.js` | | `npm run lint:fix` | ESLint with auto-fix | -| `npm run format` | Prettier on all `src/**/*.{ts,tsx}` | +| `npm run format` | Prettier on all source files | | `npm run format:check` | Prettier in check-only mode | | `npm run check` | Runs typecheck + lint + format:check together | -| `npm run bundle` | esbuild bundles cli + core + all deps → `dist/cli.js` (ESM, Node 22) | -| `npm run build` | build core (tsc) + rewrite ESM imports + bundle CLI (esbuild) | -| `npm test` | Runs all tests via `node src/tests/run-tests.mjs` | -| `npm run test:single -- ` | Run a single test file via `tsx --test` (e.g., `npm run test:single -- src/tests/session.test.ts`) | +| `npm run build` | Orchestrates full build (scripts/build.js) — compiles core + bundles CLI + copies assets | +| `npm run bundle` | Generates git commit info + esbuild bundle + copies bundled assets | +| `npm test` | Runs all workspace tests (`npm run test --workspaces --if-present`) | +| `npm run start` | Runs the locally built CLI (`scripts/start.js`) | +| `npm run build-and-start` | Builds then starts the CLI | +| `npm run clean` | Removes generated files and dist directories | + +To run a **single test file** within a package: +``` +node packages/core/src/tests/run-tests.mjs packages/core/src/tests/session.test.ts +node packages/cli/src/tests/run-tests.mjs packages/cli/src/tests/slash-commands.test.ts +``` -Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundle`). +Run the CLI locally for manual testing: `node packages/cli/dist/cli.js` (after `npm run bundle`). ## Coding Style & Naming Conventions @@ -84,9 +67,9 @@ Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundl - **Line width**: 120 characters max - **Line endings**: LF only -**TypeScript**: Strict mode enabled. Use `import type` for type-only imports (enforced by `@typescript-eslint/consistent-type-imports`). Unused variables prefixed with `_` are allowed. +**TypeScript**: Strict mode enabled (`strict: true`). Use `import type` for type-only imports (`@typescript-eslint/consistent-type-imports`). Unused variables prefixed with `_` are allowed (`argsIgnorePattern: "^_"`). Target ES2022, module ESNext with bundler resolution. JSX is `react-jsx`. -**Formatting/Linting**: Prettier + ESLint (typescript-eslint, react-hooks). Run `npm run check` before pushing. On commit, Husky + lint-staged auto-formats staged `*.{ts,tsx,js,mjs,cjs,ejs,jsx}` and `*.json` files. +**Formatting/Linting**: Prettier (double quotes, 2-space indent, semicolons) + ESLint (typescript-eslint, react-hooks). Run `npm run check` before pushing. On commit, Husky + lint-staged auto-formats staged `*.{ts,tsx,js,mjs,cjs,jsx}` and `*.json` files. **File naming**: `kebab-case.ts` for modules, `kebab-case.tsx` for React/Ink components. Test files: `*.test.ts` (always kebab-case). @@ -94,22 +77,24 @@ Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundl - **Framework**: Node.js native test runner (`node:test`) with `tsx` for TypeScript - **Assertions**: `node:assert/strict` -- **Coverage**: Target meaningful unit tests for core logic (session management, tool handlers, settings resolution, prompt buffer, permissions, MCP client, telemetry). Test files are in `src/tests/` matching the source module name. +- **Coverage**: Target meaningful unit tests for core logic (session management, tool handlers, settings resolution, prompt buffer, permissions, MCP client, telemetry). Test files are in `packages/*/src/tests/` matching the source module name. - **Test naming**: `describe`/`test` blocks with descriptive names. Example: `test("SessionManager preserves structured system content when building OpenAI messages", ...)` - **Relaxed lint rules**: Test files allow `any` and unused vars. -- Run all tests with `npm test` before submitting a PR. A cross-platform test runner is available at `src/tests/run-tests.mjs`. +- Run all tests with `npm test` before submitting a PR. Each package has its own `run-tests.mjs` cross-platform runner. ## Commit & Pull Request Guidelines -**Commit messages** follow conventional commits. From the project history: +**Commit messages** follow conventional commits: - `feat:` — new feature (e.g., `feat: add /model command`) -- `fix:` — bug fix (e.g., `fix(ui): redraw cleanly after terminal resize`) +- `fix:` — bug fix (e.g., `fix(mcp): fix Windows MCP spawn double-quoting`) - `chore:` — tooling, deps, hooks (e.g., `chore: add husky + lint-staged`) - `refactor:` — code restructuring (e.g., `refactor(ui): optimize App hooks`) -- `style:` — formatting-only changes (e.g., `style: adjust the tree structure symbols`) -- `test:` — adding or updating tests (e.g., `test: add telemetry module unit tests`) -- `docs:` — documentation (e.g., `docs: add MCP configuration guide`) +- `style:` — formatting-only changes +- `test:` — adding or updating tests +- `docs:` — documentation changes +- `perf:` — performance improvements +- `build:` — build system changes **Pull requests** should include: - A clear description of what changed and why @@ -120,22 +105,23 @@ Run the CLI locally for manual testing: `node dist/cli.js` (after `npm run bundl ## Architecture Overview -The CLI (`@vegamo/deepcode-cli`) renders a terminal UI using [Ink](https://github.com/vadimdemedes/ink) (React for terminals). `SessionManager` drives the LLM interaction loop: it builds system prompts, sends user messages with optional skills/images, streams responses, executes tool calls via `ToolExecutor`, and compacts context when token thresholds are exceeded (512K for DeepSeek V4 models, 128K for others). OpenAI client connectivity is managed by `createOpenAIClient()` in `src/common/openai-client.ts`, which caches the client singleton and applies a 180-second keep-alive timeout. +The CLI (`@vegamo/deepcode-cli`) renders a terminal UI using [Ink](https://github.com/vadimdemedes/ink) (React for terminals). `SessionManager` (in `@vegamo/deepcode-core`) drives the LLM interaction loop: it builds system prompts, sends user messages with optional skills/images, streams responses, executes tool calls via `ToolExecutor`, and compacts context when token thresholds are exceeded (512K for DeepSeek V4 models, 128K for others). OpenAI client connectivity is managed by `createOpenAIClient()` with a 180-second keep-alive timeout. -Seven built-in tools are available to the LLM: `bash`, `read`, `write`, `edit`, `AskUserQuestion`, `UpdatePlan`, and `WebSearch`. Tool definitions are registered in `src/tools/executor.ts` and described to the LLM via `src/prompt.ts` and `templates/tools/`. The `UpdatePlan` tool enables the LLM to display and update a structured task list in the terminal. +Seven built-in tools are available to the LLM: `bash`, `read`, `write`, `edit`, `AskUserQuestion`, `UpdatePlan`, and `WebSearch`. Tool definitions are registered in `packages/core/src/tools/executor.ts` and described to the LLM via `packages/core/src/prompt.ts`. -A **permission system** (`src/common/permissions.ts`) controls tool execution scopes (read/write/delete/network/git-log, etc.) with configurable allow/deny/ask decisions. The `PermissionPrompt` view presents interactive prompts when a tool requires user approval. +A **permission system** (`packages/core/src/common/permissions.ts`) controls tool execution scopes (read/write/delete/network/git-log, etc.) with configurable allow/deny/ask decisions. -A **file history system** (`src/common/file-history.ts`) provides undo/checkpoint support by creating lightweight Git branches per session. The `GitFileHistory` class manages blob storage and branch references via a `.deepcode-file-history.json` manifest. +A **file history system** (`packages/core/src/common/file-history.ts`) provides undo/checkpoint support via lightweight Git branches. -**Slash commands**: `/model`, `/new`, `/init`, `/resume`, `/continue`, `/mcp`, `/exit`, plus dynamic `/skill-name` for each loaded skill. +**Slash commands**: `/skills`, `/model`, `/new`, `/init`, `/resume`, `/continue`, `/undo`, `/mcp`, `/raw`, `/exit`, plus dynamic `/skill-name` for each loaded skill. -**Key UI features**: `@` file mentions in the prompt input (scans project files), `Ctrl+O` to view live process stdout in fullscreen, `Ctrl+V` to paste images, MCP server status display, undo selector, and permission prompts. +**Key UI features**: `@` file mentions in the prompt input, `Ctrl+O` to view live process stdout, `Ctrl+V` to paste images, Shift+Enter for newlines, MCP server status display, undo selector, and permission prompts. **CLI flags**: `-p ` / `--prompt` to auto-submit a prompt on launch, `-v` / `--version`, `-h` / `--help`. ## Agent-Specific Instructions -- **AGENTS.md loading**: The CLI loads agent instructions from `./AGENTS.md`, `./.deepcode/AGENTS.md`, or `~/.deepcode/AGENTS.md` (first found wins). Write project-specific guidance for the LLM in any of these. +- **AGENTS.md loading**: The CLI loads agent instructions from `./AGENTS.md`, `./.deepcode/AGENTS.md`, or `~/.deepcode/AGENTS.md` (first found wins). - **Skills**: Place skill definitions in `~/.agents/skills//SKILL.md` (user-level) or `./.agents/skills//SKILL.md` (project-level). Legacy path `./.deepcode/skills/` is also supported. Each SKILL.md uses YAML frontmatter with `name` and `description` fields. -- **Built-in skills**: `agent-drift-guard` (detects and corrects execution drift), `plan-and-execute` (structured task planning with progress tracking), and `karpathy-guidelines` (behavioral guidelines to reduce common LLM coding mistakes). All three are defined in `templates/skills/` and always injected into every session. +- **Built-in skills**: Four bundled skills ship with the CLI — `plan` (task planning workflow), `deepcode-self-refer` (Deep Code CLI documentation), `skill-digester` (digest & install skills), `skill-writer` (create & debug skills). Additionally, `karpathy-guidelines` (behavioral guidelines to reduce LLM coding mistakes) is injected as a default skill template. +- **Prompt file references**: Use `@path/to/file` syntax in prompts to load file contents through the read tool. From ad11d9af15bb7114d32c4da02c78394adb5a60a7 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 23 Jun 2026 16:08:24 +0800 Subject: [PATCH 185/212] =?UTF-8?q?feat(cli):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=80=9A=E8=BF=87=20--resume=20=E5=8F=82=E6=95=B0=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E4=BC=9A=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 App 组件中新增 resumeSessionId 属性以支持恢复会话功能 - 在 AppContainer 中传递 resumeSessionId 以贯穿组件树 - 在 cli.tsx 中解析并传递 --resume 参数,支持无 ID 列出会话选择 - 新增构建退出摘要时显示 resume 会话提示的逻辑 - 添加对应单元测试覆盖 resumeSessionId 解析及退出摘要展示 - 抽离 cli 参数解析函数,支持提取初始提示和恢复会话 ID - 修复退出摘要文本,添加 resume 使用提示,提升用户体验 --- packages/cli/src/cli-args.ts | 31 +++++++++ packages/cli/src/cli.tsx | 12 ++-- packages/cli/src/tests/cli-args.test.ts | 76 +++++++++++++++++++++ packages/cli/src/tests/exit-summary.test.ts | 38 +++++++++++ packages/cli/src/ui/exit-summary.ts | 9 ++- packages/cli/src/ui/views/App.tsx | 21 +++++- packages/cli/src/ui/views/AppContainer.tsx | 10 ++- 7 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 packages/cli/src/cli-args.ts create mode 100644 packages/cli/src/tests/cli-args.test.ts diff --git a/packages/cli/src/cli-args.ts b/packages/cli/src/cli-args.ts new file mode 100644 index 00000000..5ae0325e --- /dev/null +++ b/packages/cli/src/cli-args.ts @@ -0,0 +1,31 @@ +/** + * CLI argument parsing helpers. + * Extracted from cli.tsx for testability. + */ + +export function extractInitialPrompt(args: string[]): string | undefined { + const promptIndex = args.findIndex((arg) => arg === "-p" || arg === "--prompt"); + if (promptIndex !== -1 && promptIndex + 1 < args.length) { + return args[promptIndex + 1]; + } + return undefined; +} + +/** + * Extract the --resume flag value. + * + * Returns: + * - `undefined` — `--resume` was not used + * - `true` — `--resume` was used without a session ID (show session picker) + * - `string` — `--resume ` was used (resume specific session) + */ +export function extractResumeSessionId(args: string[]): string | true | undefined { + const idx = args.findIndex((arg) => arg === "--resume"); + if (idx === -1) { + return undefined; + } + if (idx + 1 < args.length && !args[idx + 1].startsWith("-")) { + return args[idx + 1]; + } + return true; +} diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index c595916b..4812f9eb 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -3,6 +3,7 @@ import { render } from "ink"; import { setShellIfWindows } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; +import { extractInitialPrompt, extractResumeSessionId } from "./cli-args"; const args = process.argv.slice(2); const packageInfo = readPackageInfo(); @@ -21,6 +22,7 @@ if (args.includes("--help") || args.includes("-h")) { " deepcode Launch the interactive TUI in the current directory", " deepcode -p Launch with a pre-filled prompt", " deepcode --prompt Same as -p", + " deepcode --resume [sessionId] Resume a specific session by its ID. Use without an ID to show session picker", " deepcode --version Print the version", " deepcode --help Show this help", "", @@ -58,15 +60,8 @@ if (args.includes("--help") || args.includes("-h")) { process.exit(0); } -function extractInitialPrompt(args: string[]): string | undefined { - const promptIndex = args.findIndex((arg) => arg === "-p" || arg === "--prompt"); - if (promptIndex !== -1 && promptIndex + 1 < args.length) { - return args[promptIndex + 1]; - } - return undefined; -} - let initialPrompt = extractInitialPrompt(args); +const resumeSessionId = extractResumeSessionId(args); const projectRoot = process.cwd(); configureWindowsShell(); @@ -94,6 +89,7 @@ async function main(): Promise { projectRoot={projectRoot} version={packageInfo.version} initialPrompt={appInitialPrompt} + resumeSessionId={resumeSessionId} onRestart={() => restartRef.current?.()} />, { exitOnCtrlC: false } diff --git a/packages/cli/src/tests/cli-args.test.ts b/packages/cli/src/tests/cli-args.test.ts new file mode 100644 index 00000000..e42a4b00 --- /dev/null +++ b/packages/cli/src/tests/cli-args.test.ts @@ -0,0 +1,76 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { extractInitialPrompt, extractResumeSessionId } from "../cli-args"; + +// ── extractInitialPrompt ───────────────────────────────────────────────────── + +test("extractInitialPrompt returns prompt after -p", () => { + assert.equal(extractInitialPrompt(["-p", "hello world"]), "hello world"); +}); + +test("extractInitialPrompt returns prompt after --prompt", () => { + assert.equal(extractInitialPrompt(["--prompt", "hello world"]), "hello world"); +}); + +test("extractInitialPrompt returns undefined when -p is not present", () => { + assert.equal(extractInitialPrompt(["--version"]), undefined); +}); + +test("extractInitialPrompt returns undefined when -p has no value", () => { + assert.equal(extractInitialPrompt(["-p"]), undefined); +}); + +test("extractInitialPrompt returns undefined for empty args", () => { + assert.equal(extractInitialPrompt([]), undefined); +}); + +test("extractInitialPrompt ignores -p in non-flag position", () => { + assert.equal(extractInitialPrompt(["--resume", "-p", "hello"]), "hello"); +}); + +// ── extractResumeSessionId ─────────────────────────────────────────────────── + +test("extractResumeSessionId returns session ID after --resume", () => { + assert.equal( + extractResumeSessionId(["--resume", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]), + "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6" + ); +}); + +test("extractResumeSessionId returns true when --resume has no value (show picker)", () => { + assert.equal(extractResumeSessionId(["--resume"]), true); +}); + +test("extractResumeSessionId returns true when --resume is followed by another flag", () => { + assert.equal(extractResumeSessionId(["--resume", "--force"]), true); +}); + +test("extractResumeSessionId returns undefined when --resume is not present", () => { + assert.equal(extractResumeSessionId(["--version"]), undefined); +}); + +test("extractResumeSessionId returns undefined for empty args", () => { + assert.equal(extractResumeSessionId([]), undefined); +}); + +test("extractResumeSessionId works with other flags after sessionId", () => { + assert.equal(extractResumeSessionId(["--resume", "abc-123", "--force"]), "abc-123"); +}); + +test("extractResumeSessionId does not confuse --resume with other args", () => { + assert.equal(extractResumeSessionId(["-p", "test"]), undefined); +}); + +// ── combined usage ─────────────────────────────────────────────────────────── + +test("extractInitialPrompt and extractResumeSessionId work independently", () => { + const args = ["--resume", "session-123", "-p", "hello"]; + assert.equal(extractResumeSessionId(args), "session-123"); + assert.equal(extractInitialPrompt(args), "hello"); +}); + +test("extractResumeSessionId with --resume and -p but no sessionId", () => { + const args = ["--resume", "-p", "hello"]; + assert.equal(extractResumeSessionId(args), true); + assert.equal(extractInitialPrompt(args), "hello"); +}); diff --git a/packages/cli/src/tests/exit-summary.test.ts b/packages/cli/src/tests/exit-summary.test.ts index e0d481db..d768c165 100644 --- a/packages/cli/src/tests/exit-summary.test.ts +++ b/packages/cli/src/tests/exit-summary.test.ts @@ -90,6 +90,44 @@ test("buildExitSummaryText does not derive usage rows from legacy aggregate usag assert.doesNotMatch(summary, /11,966/); }); +test("buildExitSummaryText shows resume hint when sessionId is provided", () => { + const sessionId = "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"; + const summary = stripAnsi( + buildExitSummaryText({ + session: buildSession(null), + sessionId, + }) + ); + + assert.match(summary, /Goodbye!/); + assert.match(summary, /deepcode --resume 0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6/); + assert.match(summary, /To continue this session/); +}); + +test("buildExitSummaryText does not show resume hint when sessionId is omitted", () => { + const summary = stripAnsi( + buildExitSummaryText({ + session: buildSession(null), + }) + ); + + assert.match(summary, /Goodbye!/); + assert.doesNotMatch(summary, /deepcode --resume/); + assert.doesNotMatch(summary, /To continue this session/); +}); + +test("buildExitSummaryText shows resume hint with null session", () => { + const summary = stripAnsi( + buildExitSummaryText({ + session: null, + sessionId: "test-session-id", + }) + ); + + assert.match(summary, /Goodbye!/); + assert.match(summary, /deepcode --resume test-session-id/); +}); + function buildSession(usage: ModelUsage | null, usagePerModel: Record | null = null): SessionEntry { return { id: "session-1", diff --git a/packages/cli/src/ui/exit-summary.ts b/packages/cli/src/ui/exit-summary.ts index 25e09b48..baef723a 100644 --- a/packages/cli/src/ui/exit-summary.ts +++ b/packages/cli/src/ui/exit-summary.ts @@ -4,6 +4,7 @@ import type { ModelUsage, SessionEntry } from "@vegamo/deepcode-core"; type ExitSummaryInput = { session: SessionEntry | null; + sessionId?: string; }; const ANSI_RE = /\u001b\[[0-9;]*[a-zA-Z]/g; @@ -67,7 +68,7 @@ function extractUsageFields(usage: ModelUsage | null): UsageFields { } export function buildExitSummaryText(input: ExitSummaryInput): string { - const { session } = input; + const { session, sessionId } = input; const innerWidth = 98; const contentWidth = innerWidth - 4; // "│ " prefix + " │" suffix → 4 chars padding @@ -134,6 +135,12 @@ export function buildExitSummaryText(input: ExitSummaryInput): string { rows.push(""); + if (sessionId) { + const resumeHint = chalk.dim(`To continue this session, run deepcode --resume ${sessionId}`); + rows.push(resumeHint); + rows.push(""); + } + const border = borderColor("─".repeat(innerWidth)); const top = `${borderColor("╭")}${border}${borderColor("╮")}`; const bottom = `${borderColor("╰")}${border}${borderColor("╯")}`; diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index fe1f81cf..afcaa26c 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -53,6 +53,7 @@ const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", type AppProps = { projectRoot: string; initialPrompt?: string; + resumeSessionId?: string | true; onRestart?: () => void; }; @@ -89,12 +90,13 @@ const StatusLine = React.memo(function StatusLine({ ); }); -function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement { +function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProps): React.ReactElement { const { exit } = useApp(); const { stdout, write } = useStdout(); const { columns, rows } = useWindowSize(); const { mode, setMode } = useRawModeContext(); const initialPromptSubmittedRef = useRef(false); + const resumeSessionIdRef = useRef(false); const processStdoutRef = useRef>(new Map()); const rawModeRef = useRef(mode); const writeRef = useRef(write); @@ -288,7 +290,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl setTimeout(() => { const activeSessionId = sessionManager.getActiveSessionId(); const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null; - const summary = buildExitSummaryText({ session }); + const summary = buildExitSummaryText({ session, sessionId: activeSessionId ?? undefined }); process.stdout.write("\n"); process.stdout.write(chalk.rgb(34, 154, 195)("> /exit ")); process.stdout.write("\n\n"); @@ -506,6 +508,21 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl [sessionManager, resetStaticView, pendingPermissionReply, refreshSkills] ); + useEffect(() => { + if (resumeSessionIdRef.current || !resumeSessionId) { + return; + } + + resumeSessionIdRef.current = true; + if (resumeSessionId === true) { + // No session ID — show the session picker (same as /resume) + refreshSessionsList(); + navigateToSubView("session-list"); + } else { + handleSelectSession(resumeSessionId); + } + }, [handleSelectSession, navigateToSubView, refreshSessionsList, resumeSessionId]); + const handleDeleteSession = useCallback( async (id: string): Promise => { const isActiveSession = sessionManager.getActiveSessionId() === id; diff --git a/packages/cli/src/ui/views/AppContainer.tsx b/packages/cli/src/ui/views/AppContainer.tsx index d5f6363a..555588f1 100644 --- a/packages/cli/src/ui/views/AppContainer.tsx +++ b/packages/cli/src/ui/views/AppContainer.tsx @@ -7,12 +7,18 @@ const AppContainer: React.FC<{ projectRoot: string; version: string; initialPrompt: string | undefined; + resumeSessionId: string | true | undefined; onRestart: () => void; -}> = ({ version, projectRoot, initialPrompt, onRestart }) => { +}> = ({ version, projectRoot, initialPrompt, resumeSessionId, onRestart }) => { return ( - + ); From 361f2b171fe72efd4d588b5730105b22626a1394 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 23 Jun 2026 16:31:37 +0800 Subject: [PATCH 186/212] =?UTF-8?q?fix(cli):=20=E9=AA=8C=E8=AF=81=20--resu?= =?UTF-8?q?me=20=E5=8F=82=E6=95=B0=E4=B8=AD=E7=9A=84=E4=BC=9A=E8=AF=9DID?= =?UTF-8?q?=E6=9C=89=E6=95=88=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在启动 TUI 之前校验传入的 resumeSessionId 是否在本地会话索引中存在 - 读取用户主目录下的 sessions-index.json 文件进行会话ID验证 - 未找到匹配会话时输出错误信息并退出进程 - 在 App 组件中移除对会话ID重复验证的注释补充说明 - 确保 resumeSessionId 已经校验通过后才调用 handleSelectSession --- packages/cli/src/cli.tsx | 22 +++++++++++++++++++++- packages/cli/src/ui/views/App.tsx | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 4812f9eb..80513f78 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,6 +1,9 @@ import React from "react"; import { render } from "ink"; -import { setShellIfWindows } from "@vegamo/deepcode-core"; +import { readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { homedir } from "node:os"; +import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; import { extractInitialPrompt, extractResumeSessionId } from "./cli-args"; @@ -65,6 +68,23 @@ const resumeSessionId = extractResumeSessionId(args); const projectRoot = process.cwd(); configureWindowsShell(); +// Validate --resume before entering TUI +if (typeof resumeSessionId === "string") { + const projectCode = getProjectCode(projectRoot); + const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json"); + try { + const index = JSON.parse(readFileSync(indexPath, "utf-8")); + const found = Array.isArray(index?.entries) && index.entries.some((e: { id: string }) => e.id === resumeSessionId); + if (!found) { + process.stderr.write(`No saved session found with ID "${resumeSessionId}".\n`); + process.exit(1); + } + } catch { + process.stderr.write(`No saved session found with ID "${resumeSessionId}".\n`); + process.exit(1); + } +} + if (!process.stdin.isTTY) { process.stderr.write("deepcode requires an interactive terminal (TTY). " + "Re-run from a real terminal session.\n"); process.exit(1); diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index afcaa26c..4175b49d 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -519,6 +519,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp refreshSessionsList(); navigateToSubView("session-list"); } else { + // Session ID already validated in cli.tsx — guaranteed to exist handleSelectSession(resumeSessionId); } }, [handleSelectSession, navigateToSubView, refreshSessionsList, resumeSessionId]); From c47b1166d767b80979dd3043103aba653889a03b Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Tue, 23 Jun 2026 16:58:14 +0800 Subject: [PATCH 187/212] feat: when defaultMode is set to allowAll, "unknown" scope permissions should be allowed by default. --- packages/core/src/common/permissions.ts | 6 ++- packages/core/src/tests/permissions.test.ts | 42 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/core/src/common/permissions.ts b/packages/core/src/common/permissions.ts index 822fcca4..f50d485c 100644 --- a/packages/core/src/common/permissions.ts +++ b/packages/core/src/common/permissions.ts @@ -273,7 +273,7 @@ export function evaluatePermissionScopes( defaultMode: "allowAll", } ): PermissionDecision { - if (scopes.includes("unknown")) { + if (scopes.includes("unknown") && settings.defaultMode !== "allowAll") { return "ask"; } if (scopes.length === 0) { @@ -304,7 +304,9 @@ export function getPermissionScopesRequiringAsk( const result: AskPermissionScope[] = []; for (const scope of scopes) { if (scope === "unknown") { - result.push(scope); + if (settings.defaultMode !== "allowAll") { + result.push(scope); + } continue; } if (settings.deny.includes(scope)) { diff --git a/packages/core/src/tests/permissions.test.ts b/packages/core/src/tests/permissions.test.ts index 5e8bf1e8..d7ca2c5e 100644 --- a/packages/core/src/tests/permissions.test.ts +++ b/packages/core/src/tests/permissions.test.ts @@ -7,6 +7,7 @@ import { appendProjectPermissionAllows, computeToolCallPermissions, evaluatePermissionScopes, + getPermissionScopesRequiringAsk, hasUserPermissionReplies, isPathInAnyDirectory, parseBashSideEffects, @@ -46,6 +47,47 @@ test("evaluatePermissionScopes applies deny, ask, allow, and default mode preced assert.equal(evaluatePermissionScopes(["unknown"], settings), "ask"); }); +test("evaluatePermissionScopes allows unknown when defaultMode is allowAll", () => { + const allowAllSettings = { + allow: [] as const, + deny: [] as const, + ask: [] as const, + defaultMode: "allowAll" as const, + }; + assert.equal(evaluatePermissionScopes(["unknown"], allowAllSettings), "allow"); + + // unknown + other scopes that would otherwise trigger ask should still ask for those scopes + const askNetworkSettings = { + allow: [] as const, + deny: [] as const, + ask: ["network" as const], + defaultMode: "allowAll" as const, + }; + assert.equal(evaluatePermissionScopes(["unknown", "network"], askNetworkSettings), "ask"); +}); + +test("getPermissionScopesRequiringAsk excludes unknown when defaultMode is allowAll", () => { + const allowAllSettings = { + allow: [] as const, + deny: [] as const, + ask: ["network" as const], + defaultMode: "allowAll" as const, + }; + const result = getPermissionScopesRequiringAsk(["unknown", "network"], allowAllSettings); + assert.deepEqual(result, ["network"]); +}); + +test("getPermissionScopesRequiringAsk includes unknown when defaultMode is askAll", () => { + const askAllSettings = { + allow: [] as const, + deny: [] as const, + ask: ["network" as const], + defaultMode: "askAll" as const, + }; + const result = getPermissionScopesRequiringAsk(["unknown", "network"], askAllSettings); + assert.deepEqual(result, ["unknown", "network"]); +}); + test("computeToolCallPermissions maps tool calls to permission requests", () => { const projectRoot = createTempDir("deepcode-permissions-workspace-"); const plan = computeToolCallPermissions({ From dc068a6935b3e14d01970a9775e6b7207397ded5 Mon Sep 17 00:00:00 2001 From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:49:17 +0800 Subject: [PATCH 188/212] fix: prevent duplicate statusline when user/project settings are the same file When the CLI is launched from ~ (home directory), user-level and project-level settings.json resolve to the same file. This caused resolveSettingsSources to merge the same content twice, resulting in duplicate statusline segments and React non-unique key warnings. Fix: detect same-file early in resolveCurrentSettings and pass null for project settings when paths are identical. --- packages/core/src/settings.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index f7c9f51f..019b7f38 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -654,9 +654,12 @@ export function writeModelConfigSelection( } export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { + const userPath = path.resolve(getUserSettingsPath()); + const projectPath = path.resolve(getProjectSettingsPath(projectRoot)); + const sameFile = userPath === projectPath; return resolveSettingsSources( readSettings(), - readProjectSettings(projectRoot), + sameFile ? null : readProjectSettings(projectRoot), { model: DEFAULT_MODEL, baseURL: DEFAULT_BASE_URL, From a2c74df41679301089c089890f24d0d05586460c Mon Sep 17 00:00:00 2001 From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:05:24 +0800 Subject: [PATCH 189/212] fix: statusline provider id dedup, newLine support, and CI type errors - Project-level providers override user-level by id instead of appending - Add newLine option to break statusline into multiple rows - Remove external deepcode-core from esbuild (bundle inline) - Fix permissions.test.ts readonly array type errors --- packages/cli/src/tests/statusline.test.ts | 29 ++++++++++++++- .../cli/src/ui/statusline/command-provider.ts | 1 + packages/cli/src/ui/statusline/manager.ts | 22 ++++++++++-- packages/cli/src/ui/statusline/types.ts | 2 ++ packages/cli/src/ui/views/PromptInput.tsx | 36 ++++++++++++++----- packages/core/src/settings.ts | 10 +++++- packages/core/src/tests/permissions.test.ts | 25 ++++++------- scripts/esbuild.config.js | 1 - 8 files changed, 100 insertions(+), 26 deletions(-) diff --git a/packages/cli/src/tests/statusline.test.ts b/packages/cli/src/tests/statusline.test.ts index fffc15a1..4417cca1 100644 --- a/packages/cli/src/tests/statusline.test.ts +++ b/packages/cli/src/tests/statusline.test.ts @@ -7,7 +7,7 @@ import { sanitizeStatusText, STATUS_SEGMENT_MAX_LENGTH } from "../ui/statusline/ import { validateModulePath, loadModuleProvider } from "../ui/statusline/module-provider"; import { createCommandStatusProvider } from "../ui/statusline/command-provider"; import { StatusLineManager } from "../ui/statusline/manager"; -import { resolveSettings } from "@vegamo/deepcode-core"; +import { resolveSettings, resolveSettingsSources } from "@vegamo/deepcode-core"; import type { ResolvedStatusLineSettings } from "@vegamo/deepcode-core"; test("sanitizeStatusText returns empty for null/undefined/empty", () => { @@ -171,6 +171,33 @@ test("loadModuleProvider succeeds for a well-formed module", async () => { } }); +test("resolveSettingsSources lets project-level providers override user-level by id", () => { + const resolved = resolveSettingsSources( + { + statusline: { + enabled: true, + providers: [ + { type: "command", id: "model", command: "echo user-model" }, + { type: "command", id: "branch", command: "echo user-branch" }, + ], + }, + }, + { + statusline: { + providers: [ + { type: "command", id: "model", command: "echo project-model" }, + { type: "command", id: "cwd", command: "echo project-cwd" }, + ], + }, + }, + { model: "default-model", baseURL: "https://default.example.com" } + ); + const ids = resolved.statusline.providers.map((p) => p.id); + assert.deepEqual(ids, ["branch", "model", "cwd"]); + const modelProvider = resolved.statusline.providers.find((p) => p.id === "model"); + assert.equal(modelProvider?.type === "command" && modelProvider.command, "echo project-model"); +}); + test("StatusLineManager emits segments after fetch and stops cleanly", async () => { const config: ResolvedStatusLineSettings = { enabled: true, diff --git a/packages/cli/src/ui/statusline/command-provider.ts b/packages/cli/src/ui/statusline/command-provider.ts index fb6327d0..c89545a4 100644 --- a/packages/cli/src/ui/statusline/command-provider.ts +++ b/packages/cli/src/ui/statusline/command-provider.ts @@ -32,6 +32,7 @@ export function createCommandStatusProvider( return { id, color: config.color, + newLine: config.newLine, maxLength: config.maxLength, fetch: ({ signal }: StatusProviderContext) => new Promise((resolve) => { diff --git a/packages/cli/src/ui/statusline/manager.ts b/packages/cli/src/ui/statusline/manager.ts index 4ebd8888..9f4015ff 100644 --- a/packages/cli/src/ui/statusline/manager.ts +++ b/packages/cli/src/ui/statusline/manager.ts @@ -11,7 +11,12 @@ function segmentsEqual(a: StatusSegment[], b: StatusSegment[]): boolean { return false; } for (let i = 0; i < a.length; i++) { - if (a[i]?.id !== b[i]?.id || a[i]?.text !== b[i]?.text || a[i]?.color !== b[i]?.color) { + if ( + a[i]?.id !== b[i]?.id || + a[i]?.text !== b[i]?.text || + a[i]?.color !== b[i]?.color || + a[i]?.newLine !== b[i]?.newLine + ) { return false; } } @@ -130,7 +135,17 @@ export class StatusLineManager { if (!resolvedPath) { return null; } - return loadModuleProvider(resolvedPath, config.color, providerId, config.timeoutMs, config.maxLength); + const provider = await loadModuleProvider( + resolvedPath, + config.color, + providerId, + config.timeoutMs, + config.maxLength + ); + if (provider && config.newLine) { + provider.newLine = true; + } + return provider; } return null; } @@ -156,6 +171,9 @@ export class StatusLineManager { if (provider.color) { segment.color = provider.color; } + if (provider.newLine) { + segment.newLine = true; + } return segment; } catch { return null; diff --git a/packages/cli/src/ui/statusline/types.ts b/packages/cli/src/ui/statusline/types.ts index 5639138c..6f687e61 100644 --- a/packages/cli/src/ui/statusline/types.ts +++ b/packages/cli/src/ui/statusline/types.ts @@ -4,6 +4,7 @@ export type StatusSegment = { id: string; text: string; color?: string; + newLine?: boolean; }; export type SessionInfo = { @@ -29,6 +30,7 @@ export type StatusProvider = { id: string; color?: string; maxLength?: number; + newLine?: boolean; fetch: (ctx: StatusProviderContext) => Promise; dispose?: () => void; }; diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index 9a24abe0..2bf720b1 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -845,15 +845,33 @@ export const PromptInput = React.memo(function PromptInput({ )} {statusLineSegments && statusLineSegments.length > 0 && ( - - {statusLineSegments.map((segment, index) => ( - - {index > 0 && {statusLineSeparator ?? " · "}} - - {segment.text} - - - ))} + + {(() => { + const lines: StatusSegment[][] = []; + let currentLine: StatusSegment[] = []; + for (const segment of statusLineSegments) { + if (segment.newLine && currentLine.length > 0) { + lines.push(currentLine); + currentLine = []; + } + currentLine.push(segment); + } + if (currentLine.length > 0) { + lines.push(currentLine); + } + return lines.map((line, lineIndex) => ( + + {line.map((segment, index) => ( + + {index > 0 && {statusLineSeparator ?? " · "}} + + {segment.text} + + + ))} + + )); + })()} )} diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index 019b7f38..5dab3b5a 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -53,6 +53,7 @@ export type StatusLineProviderConfig = cwd?: string; timeoutMs?: number; color?: string; + newLine?: boolean; maxLength?: number; } | { @@ -61,6 +62,7 @@ export type StatusLineProviderConfig = path: string; timeoutMs?: number; color?: string; + newLine?: boolean; maxLength?: number; }; @@ -278,6 +280,7 @@ function normalizeStatusLineProvider(value: unknown): StatusLineProviderConfig | typeof maxLengthRaw === "number" && Number.isFinite(maxLengthRaw) && maxLengthRaw > 0 ? Math.floor(maxLengthRaw) : undefined; + const newLine = value["newLine"] === true ? true : undefined; if (type === "command") { const command = trimString(value["command"]); @@ -292,6 +295,7 @@ function normalizeStatusLineProvider(value: unknown): StatusLineProviderConfig | cwd: cwdRaw || undefined, timeoutMs, color, + newLine, maxLength, }; } @@ -306,6 +310,7 @@ function normalizeStatusLineProvider(value: unknown): StatusLineProviderConfig | path: modulePath, timeoutMs, color, + newLine, maxLength, }; } @@ -349,7 +354,10 @@ function mergeStatusLine( ): ResolvedStatusLineSettings { const userConfig = normalizeStatusLine(userSettings?.statusline) ?? {}; const projectConfig = normalizeStatusLine(projectSettings?.statusline) ?? {}; - const providers = [...(userConfig.providers ?? []), ...(projectConfig.providers ?? [])]; + const userProviders = userConfig.providers ?? []; + const projectProviders = projectConfig.providers ?? []; + const projectIds = new Set(projectProviders.map((p) => p.id)); + const providers = [...userProviders.filter((p) => !projectIds.has(p.id)), ...projectProviders]; const enabled = projectConfig.enabled ?? userConfig.enabled ?? providers.length > 0; const refreshMs = projectConfig.refreshMs ?? userConfig.refreshMs ?? DEFAULT_STATUSLINE_REFRESH_MS; const separator = projectConfig.separator ?? userConfig.separator ?? DEFAULT_STATUSLINE_SEPARATOR; diff --git a/packages/core/src/tests/permissions.test.ts b/packages/core/src/tests/permissions.test.ts index 5e8bf1e8..bc7393c7 100644 --- a/packages/core/src/tests/permissions.test.ts +++ b/packages/core/src/tests/permissions.test.ts @@ -11,6 +11,7 @@ import { isPathInAnyDirectory, parseBashSideEffects, } from "../common/permissions"; +import type { PermissionScope } from "../settings"; const tempDirs: string[] = []; @@ -52,10 +53,10 @@ test("computeToolCallPermissions maps tool calls to permission requests", () => sessionId: "session-1", projectRoot, settings: { - allow: [], - deny: [], - ask: ["write-out-cwd", "network"], - defaultMode: "allowAll", + allow: [] as PermissionScope[], + deny: [] as PermissionScope[], + ask: ["write-out-cwd", "network"] as PermissionScope[], + defaultMode: "allowAll" as const, }, resolveSnippetPath: () => path.join(projectRoot, "src", "file.ts"), toolCalls: [ @@ -100,10 +101,10 @@ test("computeToolCallPermissions only asks for scopes not already allowed", () = sessionId: "session-1", projectRoot, settings: { - allow: ["read-in-cwd"], - deny: [], - ask: [], - defaultMode: "askAll", + allow: ["read-in-cwd"] as PermissionScope[], + deny: [] as PermissionScope[], + ask: [] as PermissionScope[], + defaultMode: "askAll" as const, }, toolCalls: [ { @@ -138,10 +139,10 @@ test("computeToolCallPermissions allows read tool calls under skill scan paths", projectRoot, readPermissionExemptPaths: [skillRoot], settings: { - allow: [], - deny: [], - ask: [], - defaultMode: "askAll", + allow: [] as PermissionScope[], + deny: [] as PermissionScope[], + ask: [] as PermissionScope[], + defaultMode: "askAll" as const, }, toolCalls: [ { diff --git a/scripts/esbuild.config.js b/scripts/esbuild.config.js index 36c174dc..bf814a32 100644 --- a/scripts/esbuild.config.js +++ b/scripts/esbuild.config.js @@ -20,7 +20,6 @@ await build({ jsx: "automatic", jsxImportSource: "react", packages: "external", - external: ["@vegamo/deepcode-core"], logOverride: { "empty-import-meta": "silent", }, From afc5ca82921c4abcba5284acaa0574676dbb7379 Mon Sep 17 00:00:00 2001 From: dengmik-commits <270912164+dengmik-commits@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:18:26 +0800 Subject: [PATCH 190/212] fix: resolve permissions.test.ts type errors from upstream merge Add PermissionSettings import and use explicit type annotations for settings objects with empty arrays (as const produces readonly never[] which is incompatible with Required). --- packages/core/src/tests/permissions.test.ts | 52 ++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/core/src/tests/permissions.test.ts b/packages/core/src/tests/permissions.test.ts index 26baecad..fd3b676a 100644 --- a/packages/core/src/tests/permissions.test.ts +++ b/packages/core/src/tests/permissions.test.ts @@ -12,7 +12,7 @@ import { isPathInAnyDirectory, parseBashSideEffects, } from "../common/permissions"; -import type { PermissionScope } from "../settings"; +import type { PermissionScope, PermissionSettings } from "../settings"; const tempDirs: string[] = []; @@ -33,11 +33,11 @@ test("parseBashSideEffects accepts valid scopes and normalizes unsafe values to }); test("evaluatePermissionScopes applies deny, ask, allow, and default mode precedence", () => { - const settings = { - allow: ["read-in-cwd" as const], - deny: ["write-out-cwd" as const], - ask: ["network" as const], - defaultMode: "askAll" as const, + const settings: Required = { + allow: ["read-in-cwd"] as PermissionScope[], + deny: ["write-out-cwd"] as PermissionScope[], + ask: ["network"] as PermissionScope[], + defaultMode: "askAll", }; assert.equal(evaluatePermissionScopes(["write-out-cwd"], settings), "deny"); @@ -49,41 +49,41 @@ test("evaluatePermissionScopes applies deny, ask, allow, and default mode preced }); test("evaluatePermissionScopes allows unknown when defaultMode is allowAll", () => { - const allowAllSettings = { - allow: [] as const, - deny: [] as const, - ask: [] as const, - defaultMode: "allowAll" as const, + const allowAllSettings: Required = { + allow: [] as PermissionScope[], + deny: [] as PermissionScope[], + ask: [] as PermissionScope[], + defaultMode: "allowAll", }; assert.equal(evaluatePermissionScopes(["unknown"], allowAllSettings), "allow"); // unknown + other scopes that would otherwise trigger ask should still ask for those scopes - const askNetworkSettings = { - allow: [] as const, - deny: [] as const, - ask: ["network" as const], - defaultMode: "allowAll" as const, + const askNetworkSettings: Required = { + allow: [] as PermissionScope[], + deny: [] as PermissionScope[], + ask: ["network"] as PermissionScope[], + defaultMode: "allowAll", }; assert.equal(evaluatePermissionScopes(["unknown", "network"], askNetworkSettings), "ask"); }); test("getPermissionScopesRequiringAsk excludes unknown when defaultMode is allowAll", () => { - const allowAllSettings = { - allow: [] as const, - deny: [] as const, - ask: ["network" as const], - defaultMode: "allowAll" as const, + const allowAllSettings: Required = { + allow: [] as PermissionScope[], + deny: [] as PermissionScope[], + ask: ["network"] as PermissionScope[], + defaultMode: "allowAll", }; const result = getPermissionScopesRequiringAsk(["unknown", "network"], allowAllSettings); assert.deepEqual(result, ["network"]); }); test("getPermissionScopesRequiringAsk includes unknown when defaultMode is askAll", () => { - const askAllSettings = { - allow: [] as const, - deny: [] as const, - ask: ["network" as const], - defaultMode: "askAll" as const, + const askAllSettings: Required = { + allow: [] as PermissionScope[], + deny: [] as PermissionScope[], + ask: ["network"] as PermissionScope[], + defaultMode: "askAll", }; const result = getPermissionScopesRequiringAsk(["unknown", "network"], askAllSettings); assert.deepEqual(result, ["unknown", "network"]); From a3952e11115236dbd0bd31dee89f954a7c1a3b08 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 24 Jun 2026 09:24:54 +0800 Subject: [PATCH 191/212] =?UTF-8?q?refactor(cli):=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20existsSync=20=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 node:fs 模块中未使用的 existsSync 导入 - 保持代码整洁,避免冗余依赖 - 优化代码可读性和维护性 --- packages/cli/src/cli.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 80513f78..6ac5372d 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,6 +1,6 @@ import React from "react"; import { render } from "ink"; -import { readFileSync, existsSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; From c8d8fa815b9a10f7fda36cff4c061ac216c48295 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Wed, 24 Jun 2026 09:59:01 +0800 Subject: [PATCH 192/212] fix: the CLI typecheck error --- packages/core/src/tests/permissions.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/tests/permissions.test.ts b/packages/core/src/tests/permissions.test.ts index d7ca2c5e..a0d1f17b 100644 --- a/packages/core/src/tests/permissions.test.ts +++ b/packages/core/src/tests/permissions.test.ts @@ -49,17 +49,17 @@ test("evaluatePermissionScopes applies deny, ask, allow, and default mode preced test("evaluatePermissionScopes allows unknown when defaultMode is allowAll", () => { const allowAllSettings = { - allow: [] as const, - deny: [] as const, - ask: [] as const, + allow: [], + deny: [], + ask: [], defaultMode: "allowAll" as const, }; assert.equal(evaluatePermissionScopes(["unknown"], allowAllSettings), "allow"); // unknown + other scopes that would otherwise trigger ask should still ask for those scopes const askNetworkSettings = { - allow: [] as const, - deny: [] as const, + allow: [], + deny: [], ask: ["network" as const], defaultMode: "allowAll" as const, }; @@ -68,8 +68,8 @@ test("evaluatePermissionScopes allows unknown when defaultMode is allowAll", () test("getPermissionScopesRequiringAsk excludes unknown when defaultMode is allowAll", () => { const allowAllSettings = { - allow: [] as const, - deny: [] as const, + allow: [], + deny: [], ask: ["network" as const], defaultMode: "allowAll" as const, }; @@ -79,8 +79,8 @@ test("getPermissionScopesRequiringAsk excludes unknown when defaultMode is allow test("getPermissionScopesRequiringAsk includes unknown when defaultMode is askAll", () => { const askAllSettings = { - allow: [] as const, - deny: [] as const, + allow: [], + deny: [], ask: ["network" as const], defaultMode: "askAll" as const, }; From f4ded9a866c157169ad6fc16b70b700283c46ac6 Mon Sep 17 00:00:00 2001 From: hcyang Date: Wed, 24 Jun 2026 10:04:33 +0800 Subject: [PATCH 193/212] =?UTF-8?q?feat(cli):=20=E5=A2=9E=E5=8A=A0Git?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=A1=E6=81=AF=E5=92=8C=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入并展示CLI版本及Git提交信息 - 优化退出摘要界面颜色,提升可读性 - 在会话恢复提示中添加高亮命令显示 - 在PackageInfo类型中添加gitCommit字段支持 --- packages/cli/src/cli.tsx | 6 ++++-- packages/cli/src/common/update-check.ts | 1 + packages/cli/src/ui/exit-summary.ts | 7 ++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 6ac5372d..48152d0b 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -7,6 +7,7 @@ import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; import { extractInitialPrompt, extractResumeSessionId } from "./cli-args"; +import { CLI_VERSION, GIT_COMMIT_INFO } from "./generated/git-commit"; const args = process.argv.slice(2); const packageInfo = readPackageInfo(); @@ -151,9 +152,10 @@ function readPackageInfo(): PackageInfo { const pkg = require("../package.json") as { name?: unknown; version?: unknown }; return { name: typeof pkg.name === "string" ? pkg.name : "@vegamo/deepcode-cli", - version: typeof pkg.version === "string" ? pkg.version : "", + version: typeof pkg.version === "string" ? pkg.version : (CLI_VERSION ?? ""), + gitCommit: GIT_COMMIT_INFO ?? "", }; } catch { - return { name: "@vegamo/deepcode-cli", version: "" }; + return { name: "@vegamo/deepcode-cli", version: CLI_VERSION ?? "", gitCommit: GIT_COMMIT_INFO ?? "" }; } } diff --git a/packages/cli/src/common/update-check.ts b/packages/cli/src/common/update-check.ts index 7a4710be..3b82e51a 100644 --- a/packages/cli/src/common/update-check.ts +++ b/packages/cli/src/common/update-check.ts @@ -10,6 +10,7 @@ import { killProcessTree } from "@vegamo/deepcode-core"; export type PackageInfo = { name: string; version: string; + gitCommit?: string; }; type UpdateState = { diff --git a/packages/cli/src/ui/exit-summary.ts b/packages/cli/src/ui/exit-summary.ts index baef723a..1a28ab8f 100644 --- a/packages/cli/src/ui/exit-summary.ts +++ b/packages/cli/src/ui/exit-summary.ts @@ -73,7 +73,7 @@ export function buildExitSummaryText(input: ExitSummaryInput): string { const innerWidth = 98; const contentWidth = innerWidth - 4; // "│ " prefix + " │" suffix → 4 chars padding - const borderColor = chalk.hex("#229ac3e6"); + const borderColor = chalk.dim; const titleColor = gradientString("#229ac3e6", "rgb(125 51 247 / 0.7)"); const line = (text: string) => `${borderColor("│")} ${padRight(text, contentWidth)} ${borderColor("│")}`; @@ -114,7 +114,7 @@ export function buildExitSummaryText(input: ExitSummaryInput): string { padLeft("Output Tokens", colOutput) + padLeft("Cached Tokens", colCached); rows.push(chalk.bold(headerRow)); - rows.push(divider); + rows.push(chalk.gray(divider)); for (const { modelName, usage } of usageRows) { const reqsStr = formatNumber(usage.totalReqs).padStart(colReqs); @@ -136,7 +136,8 @@ export function buildExitSummaryText(input: ExitSummaryInput): string { rows.push(""); if (sessionId) { - const resumeHint = chalk.dim(`To continue this session, run deepcode --resume ${sessionId}`); + const resumeHint = + chalk.dim(`To continue this session, run `) + chalk.hex("#229ac3")(`deepcode --resume ${sessionId}`); rows.push(resumeHint); rows.push(""); } From 346ecee0c988eb6e62d7e4aadf7261d03453a4f5 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 25 Jun 2026 09:00:13 +0800 Subject: [PATCH 194/212] =?UTF-8?q?refactor(cli):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E8=A1=8C=E5=8F=82=E6=95=B0=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E5=90=AF=E5=8A=A8=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 yargs 库替代原有手写解析,增强参数解析的健壮性和可维护性 - 添加严格参数校验,提升错误提示的清晰度 - 统一处理 --resume 和 --prompt 参数的组合逻辑,避免冲突使用 - 改进启动流程,确保先恢复会话再提交初始提示 - 将 resetStaticView 方法修改为异步以支持启动流程等待 - 替换部分异步调用为 await 确保顺序执行,避免竞态问题 - 更新 CLI 帮助文案,优化用户体验和信息表达 - 调整欢迎界面文本样式,增加标题加粗显示 - 添加相关单元测试 covering 新的参数解析和校验逻辑 - 引入 yargs 及其类型依赖,更新 package.json 和锁文件依赖清单 --- package-lock.json | 146 +++++++++++++++- packages/cli/package.json | 6 +- packages/cli/src/cli-args.ts | 111 ++++++++++--- packages/cli/src/cli.tsx | 124 +++++++------- packages/cli/src/tests/cli-args.test.ts | 174 +++++++++++++++----- packages/cli/src/tests/exit-summary.test.ts | 2 +- packages/cli/src/ui/views/App.tsx | 72 ++++---- packages/cli/src/ui/views/WelcomeScreen.tsx | 4 +- 8 files changed, 487 insertions(+), 152 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e4b47f0..954b83b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1647,6 +1647,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.61.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", @@ -2724,6 +2741,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/cockatiel": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", @@ -3109,7 +3186,6 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, "license": "MIT" }, "node_modules/encoding-sniffer": { @@ -3266,7 +3342,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3753,6 +3828,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", @@ -7385,6 +7469,15 @@ "node": ">=4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7409,6 +7502,49 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yauzl": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", @@ -7484,11 +7620,15 @@ "ignore": "^7.0.5", "ink": "^7.0.4", "ink-gradient": "^4.0.1", - "react": "^19.2.5" + "react": "^19.2.5", + "yargs": "^18.0.0" }, "bin": { "deepcode": "dist/cli.js" }, + "devDependencies": { + "@types/yargs": "^17.0.35" + }, "engines": { "node": ">=22" } diff --git a/packages/cli/package.json b/packages/cli/package.json index 654038ee..977bad59 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,10 @@ "ignore": "^7.0.5", "ink": "^7.0.4", "ink-gradient": "^4.0.1", - "react": "^19.2.5" + "react": "^19.2.5", + "yargs": "^18.0.0" + }, + "devDependencies": { + "@types/yargs": "^17.0.35" } } diff --git a/packages/cli/src/cli-args.ts b/packages/cli/src/cli-args.ts index 5ae0325e..3851ac00 100644 --- a/packages/cli/src/cli-args.ts +++ b/packages/cli/src/cli-args.ts @@ -1,31 +1,102 @@ /** * CLI argument parsing helpers. - * Extracted from cli.tsx for testability. + * Uses yargs for robust argument parsing and validation. */ -export function extractInitialPrompt(args: string[]): string | undefined { - const promptIndex = args.findIndex((arg) => arg === "-p" || arg === "--prompt"); - if (promptIndex !== -1 && promptIndex + 1 < args.length) { - return args[promptIndex + 1]; - } - return undefined; +import Yargs from "yargs"; + +export interface ParsedCliArgs { + /** Prompt text from -p / --prompt */ + prompt: string | undefined; + /** + * Resume session identifier: + * - `undefined` — --resume was not used + * - `true` — --resume was used without a session ID (show picker) + * - `string` — --resume was used + */ + resume: string | true | undefined; + /** True when --version / -v was passed */ + version: boolean; + /** True when --help / -h was passed */ + help: boolean; +} + +export interface CliParseError { + message: string; } /** - * Extract the --resume flag value. - * - * Returns: - * - `undefined` — `--resume` was not used - * - `true` — `--resume` was used without a session ID (show session picker) - * - `string` — `--resume ` was used (resume specific session) + * Parse CLI arguments with validation. + * Returns parsed args on success, or an error object if the arguments are invalid. */ -export function extractResumeSessionId(args: string[]): string | true | undefined { - const idx = args.findIndex((arg) => arg === "--resume"); - if (idx === -1) { - return undefined; +export function parseCliArgs(argv: string[]): ParsedCliArgs | CliParseError { + let validationError: string | null = null; + + const y = Yargs(argv) + .locale("en") + .scriptName("deepcode") + .version(false) + .help(false) + .option("version", { + alias: "v", + type: "boolean", + describe: "Print the version", + }) + .option("help", { + alias: "h", + type: "boolean", + describe: "Show this help", + }) + .option("resume", { + alias: "r", + type: "string", + describe: "Resume a specific session by its ID. Use without an ID to show session picker.", + }) + .option("prompt", { + alias: "p", + type: "string", + describe: "Submit a prompt on launch", + }) + .strict() + .exitProcess(false) + .fail((msg) => { + validationError = msg; + }) + .check((parsed) => { + // bare --resume conflicts with --prompt + if (parsed.resume === "" && parsed.prompt) { + throw new Error( + "Cannot use --resume without a session ID together with --prompt.\n" + + "Use --resume -p to resume a session and send a prompt." + ); + } + // empty prompt is meaningless + if (parsed.prompt === "") { + throw new Error("--prompt / -p requires a non-empty value."); + } + return true; + }); + + const parsed = y.parseSync() as Record; + + if (validationError) { + return { message: validationError }; } - if (idx + 1 < args.length && !args[idx + 1].startsWith("-")) { - return args[idx + 1]; + + const resumeRaw = parsed.resume as string | undefined; + let resume: ParsedCliArgs["resume"]; + if (resumeRaw === undefined) { + resume = undefined; + } else if (resumeRaw === "") { + resume = true; + } else { + resume = resumeRaw; } - return true; + + return { + prompt: parsed.prompt as string | undefined, + resume, + version: parsed.version === true, + help: parsed.help === true, + }; } diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 48152d0b..1ea702bc 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -6,69 +6,86 @@ import { homedir } from "node:os"; import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; -import { extractInitialPrompt, extractResumeSessionId } from "./cli-args"; +import { hideBin } from "yargs/helpers"; +import { parseCliArgs } from "./cli-args"; import { CLI_VERSION, GIT_COMMIT_INFO } from "./generated/git-commit"; -const args = process.argv.slice(2); +const args = hideBin(process.argv); const packageInfo = readPackageInfo(); -if (args.includes("--version") || args.includes("-v")) { +const HELP_TEXT = + [ + "", + "Usage: deepcode [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode", + "", + "Commands:", + " deepcode Launch the interactive TUI in the current directory", + "", + "Options:", + " -p, --prompt Launch with a pre-filled prompt", + " -r, --resume [sessionId] Resume a specific session by its ID. Use without an ID to show session picker", + " -v, --version Show version number", + " -h, --help Show help", + "", + "Configuration:", + " ~/.deepcode/settings.json User-level API key, model, base URL", + " ./.deepcode/settings.json Project-level settings", + " ./.deepcode/skills/*/SKILL.md Project-level native skills", + " ./.agents/skills/*/SKILL.md Project-level interoperable skills", + " ~/.deepcode/skills/*/SKILL.md User-level native skills", + " ~/.agents/skills/*/SKILL.md User-level interoperable skills", + "", + "Inside the TUI:", + " enter Send the prompt", + " shift+enter Insert a newline", + " home/end Move within the current line", + " alt+left/right Move by word", + " ctrl+w Delete the previous word", + " ctrl+v Paste an image from the clipboard", + " ctrl+x Clear pasted images", + " esc Interrupt the current model turn", + " / Open the skills/commands menu", + " /skills List available skills", + " /model Select model, thinking mode and effort control", + " /new Start a fresh conversation", + " /init Initialize an AGENTS.md file with instructions for LLM", + " /resume Pick a previous conversation to continue", + " /continue Continue the active conversation, or resume one if empty", + " /undo Restore code and/or conversation to a previous point", + " /mcp Show MCP server status and available tools", + " /raw Toggle display mode for viewing or collapsing reasoning content", + " /exit Quit", + " ctrl+d twice Quit", + ].join("\n") + "\n"; + +const parsed = parseCliArgs(args); + +if ("message" in parsed) { + process.stderr.write(parsed.message + "\n\n"); + process.stdout.write(HELP_TEXT); + process.exit(1); +} + +if (parsed.version) { process.stdout.write(`${packageInfo.version || "unknown"}\n`); process.exit(0); } -if (args.includes("--help") || args.includes("-h")) { - process.stdout.write( - [ - "deepcode - Deep Code CLI", - "", - "Usage:", - " deepcode Launch the interactive TUI in the current directory", - " deepcode -p Launch with a pre-filled prompt", - " deepcode --prompt Same as -p", - " deepcode --resume [sessionId] Resume a specific session by its ID. Use without an ID to show session picker", - " deepcode --version Print the version", - " deepcode --help Show this help", - "", - "Configuration:", - " ~/.deepcode/settings.json User-level API key, model, base URL", - " ./.deepcode/settings.json Project-level settings", - " ./.deepcode/skills/*/SKILL.md Project-level native skills", - " ./.agents/skills/*/SKILL.md Project-level interoperable skills", - " ~/.deepcode/skills/*/SKILL.md User-level native skills", - " ~/.agents/skills/*/SKILL.md User-level interoperable skills", - "", - "Inside the TUI:", - " enter Send the prompt", - " shift+enter Insert a newline", - " home/end Move within the current line", - " alt+left/right Move by word", - " ctrl+w Delete the previous word", - " ctrl+v Paste an image from the clipboard", - " ctrl+x Clear pasted images", - " esc Interrupt the current model turn", - " / Open the skills/commands menu", - " /skills List available skills", - " /model Select model, thinking mode and effort control", - " /new Start a fresh conversation", - " /init Initialize an AGENTS.md file with instructions for LLM", - " /resume Pick a previous conversation to continue", - " /continue Continue the active conversation, or resume one if empty", - " /undo Restore code and/or conversation to a previous point", - " /mcp Show MCP server status and available tools", - " /raw Toggle display mode for viewing or collapsing reasoning content", - " /exit Quit", - " ctrl+d twice Quit", - ].join("\n") + "\n" - ); +if (parsed.help) { + process.stdout.write(HELP_TEXT); process.exit(0); } -let initialPrompt = extractInitialPrompt(args); -const resumeSessionId = extractResumeSessionId(args); +let initialPrompt = parsed.prompt; +let resumeSessionId = parsed.resume; const projectRoot = process.cwd(); configureWindowsShell(); +if (!process.stdin.isTTY) { + process.stderr.write("deepcode requires an interactive terminal (TTY). " + "Re-run from a real terminal session.\n"); + process.exit(1); +} + // Validate --resume before entering TUI if (typeof resumeSessionId === "string") { const projectCode = getProjectCode(projectRoot); @@ -86,11 +103,6 @@ if (typeof resumeSessionId === "string") { } } -if (!process.stdin.isTTY) { - process.stderr.write("deepcode requires an interactive terminal (TTY). " + "Re-run from a real terminal session.\n"); - process.exit(1); -} - void main(); async function main(): Promise { @@ -105,12 +117,14 @@ async function main(): Promise { let restarting = false; const appInitialPrompt = initialPrompt; initialPrompt = undefined; + const appResumeSessionId = resumeSessionId; + resumeSessionId = undefined; const inkInstance = render( restartRef.current?.()} />, { exitOnCtrlC: false } diff --git a/packages/cli/src/tests/cli-args.test.ts b/packages/cli/src/tests/cli-args.test.ts index e42a4b00..fd97de76 100644 --- a/packages/cli/src/tests/cli-args.test.ts +++ b/packages/cli/src/tests/cli-args.test.ts @@ -1,76 +1,168 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { extractInitialPrompt, extractResumeSessionId } from "../cli-args"; +import { parseCliArgs } from "../cli-args"; -// ── extractInitialPrompt ───────────────────────────────────────────────────── +// ── parseCliArgs: basic parsing ────────────────────────────────────────────── -test("extractInitialPrompt returns prompt after -p", () => { - assert.equal(extractInitialPrompt(["-p", "hello world"]), "hello world"); +test("parseCliArgs returns prompt after -p", () => { + const r = parseCliArgs(["-p", "hello world"]); + assert.ok(!("message" in r)); + assert.equal(r.prompt, "hello world"); }); -test("extractInitialPrompt returns prompt after --prompt", () => { - assert.equal(extractInitialPrompt(["--prompt", "hello world"]), "hello world"); +test("parseCliArgs returns prompt after --prompt", () => { + const r = parseCliArgs(["--prompt", "hello world"]); + assert.ok(!("message" in r)); + assert.equal(r.prompt, "hello world"); }); -test("extractInitialPrompt returns undefined when -p is not present", () => { - assert.equal(extractInitialPrompt(["--version"]), undefined); +test("parseCliArgs returns undefined prompt when -p is not present", () => { + const r = parseCliArgs(["--resume"]); + assert.ok(!("message" in r)); + assert.equal(r.prompt, undefined); }); -test("extractInitialPrompt returns undefined when -p has no value", () => { - assert.equal(extractInitialPrompt(["-p"]), undefined); +test("parseCliArgs returns session ID after --resume", () => { + const r = parseCliArgs(["--resume", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]); + assert.ok(!("message" in r)); + assert.equal(r.resume, "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"); }); -test("extractInitialPrompt returns undefined for empty args", () => { - assert.equal(extractInitialPrompt([]), undefined); +test("parseCliArgs returns true when --resume has no value", () => { + const r = parseCliArgs(["--resume"]); + assert.ok(!("message" in r)); + assert.equal(r.resume, true); }); -test("extractInitialPrompt ignores -p in non-flag position", () => { - assert.equal(extractInitialPrompt(["--resume", "-p", "hello"]), "hello"); +test("parseCliArgs returns undefined resume when not present", () => { + const r = parseCliArgs(["-p", "test"]); + assert.ok(!("message" in r)); + assert.equal(r.resume, undefined); }); -// ── extractResumeSessionId ─────────────────────────────────────────────────── +test("parseCliArgs returns defaults for empty args", () => { + const r = parseCliArgs([]); + assert.ok(!("message" in r)); + assert.equal(r.prompt, undefined); + assert.equal(r.resume, undefined); + assert.equal(r.version, false); + assert.equal(r.help, false); +}); + +// ── parseCliArgs: -r alias ─────────────────────────────────────────────────── + +test("parseCliArgs returns session ID after -r", () => { + const r = parseCliArgs(["-r", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]); + assert.ok(!("message" in r)); + assert.equal(r.resume, "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"); +}); + +test("parseCliArgs returns true when -r has no value", () => { + const r = parseCliArgs(["-r"]); + assert.ok(!("message" in r)); + assert.equal(r.resume, true); +}); + +test("parseCliArgs handles -r combined with -p", () => { + const r = parseCliArgs(["-r", "session-123", "-p", "hello"]); + assert.ok(!("message" in r)); + assert.equal(r.resume, "session-123"); + assert.equal(r.prompt, "hello"); +}); + +test("parseCliArgs rejects bare -r with -p", () => { + const r = parseCliArgs(["-r", "-p", "hello"]); + assert.ok("message" in r); + assert.match(r.message, /Cannot use --resume/); +}); -test("extractResumeSessionId returns session ID after --resume", () => { - assert.equal( - extractResumeSessionId(["--resume", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]), - "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6" - ); +// ── parseCliArgs: --version / --help ───────────────────────────────────────── + +test("parseCliArgs detects --version", () => { + const r = parseCliArgs(["--version"]); + assert.ok(!("message" in r)); + assert.equal(r.version, true); + assert.equal(r.help, false); +}); + +test("parseCliArgs detects -v", () => { + const r = parseCliArgs(["-v"]); + assert.ok(!("message" in r)); + assert.equal(r.version, true); +}); + +test("parseCliArgs detects --help", () => { + const r = parseCliArgs(["--help"]); + assert.ok(!("message" in r)); + assert.equal(r.help, true); + assert.equal(r.version, false); }); -test("extractResumeSessionId returns true when --resume has no value (show picker)", () => { - assert.equal(extractResumeSessionId(["--resume"]), true); +test("parseCliArgs detects -h", () => { + const r = parseCliArgs(["-h"]); + assert.ok(!("message" in r)); + assert.equal(r.help, true); }); -test("extractResumeSessionId returns true when --resume is followed by another flag", () => { - assert.equal(extractResumeSessionId(["--resume", "--force"]), true); +test("parseCliArgs version and help are false when not passed", () => { + const r = parseCliArgs(["-p", "hello"]); + assert.ok(!("message" in r)); + assert.equal(r.version, false); + assert.equal(r.help, false); }); -test("extractResumeSessionId returns undefined when --resume is not present", () => { - assert.equal(extractResumeSessionId(["--version"]), undefined); +test("parseCliArgs handles -v combined with -r (both flags set)", () => { + const r = parseCliArgs(["-v", "-r", "abc"]); + assert.ok(!("message" in r)); + assert.equal(r.version, true); + assert.equal(r.resume, "abc"); }); -test("extractResumeSessionId returns undefined for empty args", () => { - assert.equal(extractResumeSessionId([]), undefined); +// ── parseCliArgs: combined usage ───────────────────────────────────────────── + +test("parseCliArgs handles --resume combined with -p", () => { + const r = parseCliArgs(["--resume", "session-123", "-p", "hello"]); + assert.ok(!("message" in r)); + assert.equal(r.resume, "session-123"); + assert.equal(r.prompt, "hello"); }); -test("extractResumeSessionId works with other flags after sessionId", () => { - assert.equal(extractResumeSessionId(["--resume", "abc-123", "--force"]), "abc-123"); +test("parseCliArgs handles -p before --resume ", () => { + const r = parseCliArgs(["-p", "hello", "--resume", "session-123"]); + assert.ok(!("message" in r)); + assert.equal(r.resume, "session-123"); + assert.equal(r.prompt, "hello"); }); -test("extractResumeSessionId does not confuse --resume with other args", () => { - assert.equal(extractResumeSessionId(["-p", "test"]), undefined); +// ── parseCliArgs: validation ───────────────────────────────────────────────── + +test("parseCliArgs rejects bare --resume with -p", () => { + const r = parseCliArgs(["--resume", "-p", "hello"]); + assert.ok("message" in r); + assert.match(r.message, /Cannot use --resume/); }); -// ── combined usage ─────────────────────────────────────────────────────────── +test("parseCliArgs rejects -p with bare --resume (reversed order)", () => { + const r = parseCliArgs(["-p", "hello", "--resume"]); + assert.ok("message" in r); + assert.match(r.message, /Cannot use --resume/); +}); + +test("parseCliArgs rejects unknown flags in strict mode", () => { + const r = parseCliArgs(["--unknown-flag"]); + assert.ok("message" in r); + assert.match(r.message, /Unknown argument/); +}); -test("extractInitialPrompt and extractResumeSessionId work independently", () => { - const args = ["--resume", "session-123", "-p", "hello"]; - assert.equal(extractResumeSessionId(args), "session-123"); - assert.equal(extractInitialPrompt(args), "hello"); +test("parseCliArgs rejects empty -p value", () => { + const r = parseCliArgs(["-p", ""]); + assert.ok("message" in r); + assert.match(r.message, /non-empty/); }); -test("extractResumeSessionId with --resume and -p but no sessionId", () => { - const args = ["--resume", "-p", "hello"]; - assert.equal(extractResumeSessionId(args), true); - assert.equal(extractInitialPrompt(args), "hello"); +test("parseCliArgs --version takes precedence over --help", () => { + const r = parseCliArgs(["--version", "--help"]); + assert.ok(!("message" in r)); + assert.equal(r.version, true); + assert.equal(r.help, true); }); diff --git a/packages/cli/src/tests/exit-summary.test.ts b/packages/cli/src/tests/exit-summary.test.ts index d768c165..fd6b8ad0 100644 --- a/packages/cli/src/tests/exit-summary.test.ts +++ b/packages/cli/src/tests/exit-summary.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { buildExitSummaryText } from "../ui"; import type { ModelUsage, SessionEntry } from "@vegamo/deepcode-core"; -const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, ""); +const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, ""); test("buildExitSummaryText only shows Goodbye and model usage with cached tokens", () => { const summary = stripAnsi( diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index 4175b49d..5e345802 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -97,6 +97,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp const { mode, setMode } = useRawModeContext(); const initialPromptSubmittedRef = useRef(false); const resumeSessionIdRef = useRef(false); + const startupDoneRef = useRef(false); const processStdoutRef = useRef>(new Map()); const rawModeRef = useRef(mode); const writeRef = useRef(write); @@ -190,17 +191,20 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp * Reset the static view to the welcome screen. */ const resetStaticView = useCallback( - (loadedMessages: SessionMessage[], options?: { clearScreen?: boolean }) => { + (loadedMessages: SessionMessage[], options?: { clearScreen?: boolean }): Promise => { if (options?.clearScreen) { process.stdout.write(ANSI_CLEAR_SCREEN); } setMessages([]); setWelcomeNonce((n) => n + 1); navigateToSubView("chat"); - setTimeout(() => { - setMessages(loadedMessages); - setShowWelcome(true); - }, 0); + return new Promise((resolve) => { + setTimeout(() => { + setMessages(loadedMessages); + setShowWelcome(true); + resolve(); + }, 0); + }); }, [navigateToSubView] ); @@ -246,7 +250,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp setActiveAskPermissions(undefined); setPendingPermissionReply(null); setDismissedQuestionIds(new Set()); - resetStaticView([]); + await resetStaticView([]); await refreshSkills(); }, [sessionManager, resetStaticView, refreshSkills]); @@ -477,24 +481,11 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp [resetStaticView, sessionManager] ); - useEffect(() => { - if (initialPromptSubmittedRef.current || !initialPrompt || !initialPrompt.trim()) { - return; - } - - initialPromptSubmittedRef.current = true; - handleSubmit({ - text: initialPrompt, - imageUrls: [], - selectedSkills: undefined, - }); - }, [handleSubmit, initialPrompt]); - const handleSelectSession = useCallback( async (sessionId: string) => { sessionManager.setActiveSessionId(sessionId); // Clear first so resets its index to 0. - resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true }); + await resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true }); const session = sessionManager.getSession(sessionId); setStatusLine(session ? buildStatusLine(session) : ""); setRunningProcesses(session?.processes ?? null); @@ -508,21 +499,42 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp [sessionManager, resetStaticView, pendingPermissionReply, refreshSkills] ); + /** + * Coordinated startup effect: handle --resume and --prompt together. + * When both are present, resume the session first, then submit the prompt. + */ useEffect(() => { - if (resumeSessionIdRef.current || !resumeSessionId) { + if (startupDoneRef.current) { return; } + startupDoneRef.current = true; + + async function run() { + // Step 1: Resume session if requested + if (resumeSessionId) { + resumeSessionIdRef.current = true; + if (resumeSessionId === true) { + // Bare --resume — show session picker; prompt makes no sense here + refreshSessionsList(); + navigateToSubView("session-list"); + return; + } + await handleSelectSession(resumeSessionId); + } - resumeSessionIdRef.current = true; - if (resumeSessionId === true) { - // No session ID — show the session picker (same as /resume) - refreshSessionsList(); - navigateToSubView("session-list"); - } else { - // Session ID already validated in cli.tsx — guaranteed to exist - handleSelectSession(resumeSessionId); + // Step 2: Submit prompt if provided + if (initialPrompt && initialPrompt.trim()) { + initialPromptSubmittedRef.current = true; + handleSubmit({ + text: initialPrompt, + imageUrls: [], + selectedSkills: undefined, + }); + } } - }, [handleSelectSession, navigateToSubView, refreshSessionsList, resumeSessionId]); + + void run(); + }, [handleSubmit, handleSelectSession, initialPrompt, navigateToSubView, refreshSessionsList, resumeSessionId]); const handleDeleteSession = useCallback( async (id: string): Promise => { diff --git a/packages/cli/src/ui/views/WelcomeScreen.tsx b/packages/cli/src/ui/views/WelcomeScreen.tsx index fdcf9211..e465a2f6 100644 --- a/packages/cli/src/ui/views/WelcomeScreen.tsx +++ b/packages/cli/src/ui/views/WelcomeScreen.tsx @@ -58,7 +58,9 @@ export function WelcomeScreen({ projectRoot, settings, skills, width }: WelcomeS paddingX={1} > - {">"}_ Deep Code + + {">"}_ Deep Code{" "} + (v{version || "unknown"}) {!compact ? : null} From 24a2ad934e2d59caf74a5a0515432603d618dc04 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 25 Jun 2026 14:37:41 +0800 Subject: [PATCH 195/212] =?UTF-8?q?refactor(cli):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E8=A1=8C=E5=8F=82=E6=95=B0=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=92=8C=E4=B8=BB=E5=85=A5=E5=8F=A3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用异步方式获取包信息,替代同步读取package.json - 用yargs重构参数解析,加入严格校验和格式验证 - 新增UUID格式验证函数,确保会话ID合法性 - 改善错误输出,统一通过writeStderrLine打印错误信息 - 移除过时的手工参数解析逻辑,改用parseArguments异步解析 - 统一并简化应用启动流程,支持终端交互性检查 - 替换process.stdout.write为writeStdoutLine,增强代码一致性 - 增加对版本号、帮助信息参数的自动处理和退出 - 使用read-package-up获取package.json,保证包信息准确 - 测试覆盖parseArguments及isValidSessionId的多种场景和错误处理 - 修改构建脚本为异步导入fs模块的chmodSync操作,兼容现代Node版本 --- package-lock.json | 121 ++++++++++++-- packages/cli/package.json | 3 +- packages/cli/src/cli-args.ts | 166 +++++++++++++------- packages/cli/src/cli.tsx | 146 +++++------------ packages/cli/src/common/update-check.ts | 9 +- packages/cli/src/tests/cli-args.test.ts | 199 ++++++++++++++---------- packages/cli/src/utils/package.ts | 29 ++++ packages/cli/src/utils/stdioHelpers.ts | 25 +++ packages/cli/src/utils/version.ts | 6 + 9 files changed, 447 insertions(+), 257 deletions(-) create mode 100644 packages/cli/src/utils/package.ts create mode 100644 packages/cli/src/utils/stdioHelpers.ts create mode 100644 packages/cli/src/utils/version.ts diff --git a/package-lock.json b/package-lock.json index 954b83b3..66cf2bce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -242,7 +242,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -384,7 +383,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1614,7 +1612,6 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -3732,6 +3729,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -4359,7 +4368,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4630,7 +4638,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5690,7 +5697,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -5708,7 +5714,6 @@ "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -5870,7 +5875,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -6130,6 +6134,101 @@ "node": ">=0.8" } }, + "node_modules/read-package-up": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", + "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.1", + "read-pkg": "^10.0.0", + "type-fest": "^5.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-package-up/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/read-package-up/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/read-package-up/node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/read-package-up/node_modules/read-pkg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz", + "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.4.4", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-package-up/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/read-package-up/node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/read-pkg": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", @@ -6584,7 +6683,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -6595,14 +6693,12 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -6613,7 +6709,6 @@ "version": "3.0.23", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", - "dev": true, "license": "CC0-1.0" }, "node_modules/sprintf-js": { @@ -7286,7 +7381,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", @@ -7621,6 +7715,7 @@ "ink": "^7.0.4", "ink-gradient": "^4.0.1", "react": "^19.2.5", + "read-package-up": "^12.0.0", "yargs": "^18.0.0" }, "bin": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 977bad59..1f657304 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -25,7 +25,7 @@ "scripts": { "typecheck": "tsc -p ./ --noEmit", "bundle": "node ../../scripts/esbuild.config.js", - "build": "npm run typecheck && npm run bundle && node ../../scripts/copy-bundle-assets.js && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"", + "build": "npm run typecheck && npm run bundle && node ../../scripts/copy-bundle-assets.js && node -e \"import('node:fs').then(f => f.chmodSync('dist/cli.js', 0o755))\"", "prepublishOnly": "npm run build", "format": "prettier --write .", "test": "node src/tests/run-tests.mjs" @@ -38,6 +38,7 @@ "ink": "^7.0.4", "ink-gradient": "^4.0.1", "react": "^19.2.5", + "read-package-up": "^12.0.0", "yargs": "^18.0.0" }, "devDependencies": { diff --git a/packages/cli/src/cli-args.ts b/packages/cli/src/cli-args.ts index 3851ac00..780869c0 100644 --- a/packages/cli/src/cli-args.ts +++ b/packages/cli/src/cli-args.ts @@ -3,7 +3,21 @@ * Uses yargs for robust argument parsing and validation. */ +import type { Argv } from "yargs"; import Yargs from "yargs"; +import { getCliVersion } from "./utils/version"; +import { writeStderrLine } from "./utils/stdioHelpers"; +import { hideBin } from "yargs/helpers"; + +// UUID v4 regex pattern for validation +const SESSION_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Validates if a string is a valid session ID format. + */ +export function isValidSessionId(value: string): boolean { + return SESSION_ID_REGEX.test(value); +} export interface ParsedCliArgs { /** Prompt text from -p / --prompt */ @@ -21,68 +35,112 @@ export interface ParsedCliArgs { help: boolean; } -export interface CliParseError { - message: string; +const EPILOG = [ + "Configuration:", + " ~/.deepcode/settings.json User-level API key, model, base URL", + " ./.deepcode/settings.json Project-level settings", + " ./.deepcode/skills/*/SKILL.md Project-level native skills", + " ./.agents/skills/*/SKILL.md Project-level interoperable skills", + " ~/.deepcode/skills/*/SKILL.md User-level native skills", + " ~/.agents/skills/*/SKILL.md User-level interoperable skills", + "", + "Inside the TUI:", + " enter Send the prompt", + " shift+enter Insert a newline", + " home/end Move within the current line", + " alt+left/right Move by word", + " ctrl+w Delete the previous word", + " ctrl+v Paste an image from the clipboard", + " ctrl+x Clear pasted images", + " esc Interrupt the current model turn", + " / Open the skills/commands menu", + " /skills List available skills", + " /model Select model, thinking mode and effort control", + " /new Start a fresh conversation", + " /init Initialize an AGENTS.md file with instructions for LLM", + " /resume Pick a previous conversation to continue", + " /continue Continue the active conversation, or resume one if empty", + " /undo Restore code and/or conversation to a previous point", + " /mcp Show MCP server status and available tools", + " /raw Toggle display mode for viewing or collapsing reasoning content", + " /exit Quit", + " ctrl+d twice Quit", +].join("\n"); + +async function configureYargs(argv?: string[]) { + const rawArgv = argv ?? hideBin(process.argv); + const yargsInstance = Yargs(rawArgv) + .locale("en") + .scriptName("deepcode") + .usage( + "Usage: $0 [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode" + ) + .command("$0 [query..]", "Launch Deep Code CLI", (yargsInstance: Argv) => + yargsInstance + .option("prompt", { + alias: "p", + type: "string", + describe: "Submit a prompt on launch", + }) + .option("resume", { + alias: "r", + type: "string", + describe: "Resume a specific session by its ID. Use without an ID to show session picker.", + }) + .check((argv: { [x: string]: unknown }) => { + const query = argv["query"] as string | string[] | undefined; + const hasPositionalQuery = Array.isArray(query) ? query.length > 0 : !!query; + + if (argv["prompt"] && hasPositionalQuery) { + return "Cannot use both a positional prompt and the --prompt (-p) flag together"; + } + // bare --resume conflicts with --prompt + if (argv["resume"] === "" && argv["prompt"]) { + return "Cannot use --resume without a session ID together with --prompt.\nUse --resume -p to resume a session and send a prompt."; + } + // validate --resume format if provided + if (argv["resume"] && argv["resume"] !== "" && !isValidSessionId(argv["resume"] as string)) { + return `Invalid session ID: "${argv["resume"]}". Must be a valid UUID (e.g., "123e4567-e89b-12d3-a456-426614174000").`; + } + // empty prompt is meaningless + if (argv["prompt"] === "") { + return "--prompt / -p requires a non-empty value."; + } + return true; + }) + ) + .example("deepcode", "Launch the interactive TUI in the current directory") + .example("deepcode -p ", "Launch with a pre-filled prompt") + .example("deepcode -r, --resume [sessionId]", "Resume a session or show session picker") + .epilog(EPILOG) + .strict() + .demandCommand(0, 0) + .wrap(Math.min(process.stdout.columns || 80, 120)); + yargsInstance + .version(await getCliVersion()) + .alias("v", "version") + .help() + .alias("h", "help"); + yargsInstance.wrap(yargsInstance.terminalWidth()); + return yargsInstance; } /** * Parse CLI arguments with validation. - * Returns parsed args on success, or an error object if the arguments are invalid. + * + * On validation failure the `.fail()` handler prints the error, shows help, + * and calls `process.exit(1)`, so this function always either returns a + * valid `ParsedCliArgs` or terminates the process. */ -export function parseCliArgs(argv: string[]): ParsedCliArgs | CliParseError { - let validationError: string | null = null; - - const y = Yargs(argv) - .locale("en") - .scriptName("deepcode") - .version(false) - .help(false) - .option("version", { - alias: "v", - type: "boolean", - describe: "Print the version", - }) - .option("help", { - alias: "h", - type: "boolean", - describe: "Show this help", - }) - .option("resume", { - alias: "r", - type: "string", - describe: "Resume a specific session by its ID. Use without an ID to show session picker.", - }) - .option("prompt", { - alias: "p", - type: "string", - describe: "Submit a prompt on launch", - }) - .strict() - .exitProcess(false) - .fail((msg) => { - validationError = msg; - }) - .check((parsed) => { - // bare --resume conflicts with --prompt - if (parsed.resume === "" && parsed.prompt) { - throw new Error( - "Cannot use --resume without a session ID together with --prompt.\n" + - "Use --resume -p to resume a session and send a prompt." - ); - } - // empty prompt is meaningless - if (parsed.prompt === "") { - throw new Error("--prompt / -p requires a non-empty value."); - } - return true; - }); +export async function parseArguments(argv?: string[]): Promise { + const y = (await configureYargs(argv)).exitProcess(false).fail((msg, _err, yargs) => { + writeStderrLine(msg || _err?.message || "Unknown error"); + yargs.showHelp(); + process.exit(1); + }); const parsed = y.parseSync() as Record; - if (validationError) { - return { message: validationError }; - } - const resumeRaw = parsed.resume as string | undefined; let resume: ParsedCliArgs["resume"]; if (resumeRaw === undefined) { diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 1ea702bc..edf5fdbd 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -4,108 +4,53 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; -import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; +import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check"; import { AppContainer } from "./ui"; -import { hideBin } from "yargs/helpers"; -import { parseCliArgs } from "./cli-args"; -import { CLI_VERSION, GIT_COMMIT_INFO } from "./generated/git-commit"; +import { parseArguments } from "./cli-args"; +import { writeStderrLine, writeStdoutLine } from "./utils/stdioHelpers"; +import { getPackageJson } from "./utils/package"; +import { CLI_VERSION } from "./generated/git-commit"; -const args = hideBin(process.argv); -const packageInfo = readPackageInfo(); - -const HELP_TEXT = - [ - "", - "Usage: deepcode [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode", - "", - "Commands:", - " deepcode Launch the interactive TUI in the current directory", - "", - "Options:", - " -p, --prompt Launch with a pre-filled prompt", - " -r, --resume [sessionId] Resume a specific session by its ID. Use without an ID to show session picker", - " -v, --version Show version number", - " -h, --help Show help", - "", - "Configuration:", - " ~/.deepcode/settings.json User-level API key, model, base URL", - " ./.deepcode/settings.json Project-level settings", - " ./.deepcode/skills/*/SKILL.md Project-level native skills", - " ./.agents/skills/*/SKILL.md Project-level interoperable skills", - " ~/.deepcode/skills/*/SKILL.md User-level native skills", - " ~/.agents/skills/*/SKILL.md User-level interoperable skills", - "", - "Inside the TUI:", - " enter Send the prompt", - " shift+enter Insert a newline", - " home/end Move within the current line", - " alt+left/right Move by word", - " ctrl+w Delete the previous word", - " ctrl+v Paste an image from the clipboard", - " ctrl+x Clear pasted images", - " esc Interrupt the current model turn", - " / Open the skills/commands menu", - " /skills List available skills", - " /model Select model, thinking mode and effort control", - " /new Start a fresh conversation", - " /init Initialize an AGENTS.md file with instructions for LLM", - " /resume Pick a previous conversation to continue", - " /continue Continue the active conversation, or resume one if empty", - " /undo Restore code and/or conversation to a previous point", - " /mcp Show MCP server status and available tools", - " /raw Toggle display mode for viewing or collapsing reasoning content", - " /exit Quit", - " ctrl+d twice Quit", - ].join("\n") + "\n"; - -const parsed = parseCliArgs(args); - -if ("message" in parsed) { - process.stderr.write(parsed.message + "\n\n"); - process.stdout.write(HELP_TEXT); - process.exit(1); -} +configureWindowsShell(); +void main(); -if (parsed.version) { - process.stdout.write(`${packageInfo.version || "unknown"}\n`); - process.exit(0); -} +async function main(): Promise { + const packageInfo = await getPackageJson(); + const parsed = await parseArguments(); -if (parsed.help) { - process.stdout.write(HELP_TEXT); - process.exit(0); -} + // --version and --help are handled by yargs internally (prints output as side effect) + // but with .exitProcess(false) we need to exit manually. + if (parsed.version || parsed.help) { + process.exit(0); + } -let initialPrompt = parsed.prompt; -let resumeSessionId = parsed.resume; -const projectRoot = process.cwd(); -configureWindowsShell(); + let initialPrompt = parsed.prompt; + let resumeSessionId = parsed.resume; + const projectRoot = process.cwd(); -if (!process.stdin.isTTY) { - process.stderr.write("deepcode requires an interactive terminal (TTY). " + "Re-run from a real terminal session.\n"); - process.exit(1); -} + if (!process.stdin.isTTY) { + writeStderrLine("deepcode requires an interactive terminal (TTY). Re-run from a real terminal session.\n"); + process.exit(1); + } -// Validate --resume before entering TUI -if (typeof resumeSessionId === "string") { - const projectCode = getProjectCode(projectRoot); - const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json"); - try { - const index = JSON.parse(readFileSync(indexPath, "utf-8")); - const found = Array.isArray(index?.entries) && index.entries.some((e: { id: string }) => e.id === resumeSessionId); - if (!found) { - process.stderr.write(`No saved session found with ID "${resumeSessionId}".\n`); + // Validate --resume before entering TUI + if (typeof resumeSessionId === "string") { + const projectCode = getProjectCode(projectRoot); + const indexPath = join(homedir(), ".deepcode", "projects", projectCode, "sessions-index.json"); + try { + const index = JSON.parse(readFileSync(indexPath, "utf-8")); + const found = + Array.isArray(index?.entries) && index.entries.some((e: { id: string }) => e.id === resumeSessionId); + if (!found) { + writeStderrLine(`No saved session found with ID "${resumeSessionId}".\n`); + process.exit(1); + } + } catch { + writeStderrLine(`No saved session found with ID "${resumeSessionId}".\n`); process.exit(1); } - } catch { - process.stderr.write(`No saved session found with ID "${resumeSessionId}".\n`); - process.exit(1); } -} - -void main(); -async function main(): Promise { const updatePromptResult = await promptForPendingUpdate(packageInfo); if (updatePromptResult.installed) { process.exit(0); @@ -122,7 +67,7 @@ async function main(): Promise { const inkInstance = render( restartRef.current?.()} @@ -132,7 +77,7 @@ async function main(): Promise { restartRef.current = () => { restarting = true; - process.stdout.write("\u001B[2J\u001B[3J\u001B[H"); + writeStdoutLine("\u001B[2J\u001B[3J\u001B[H"); inkInstance.unmount(); startApp(); }; @@ -156,20 +101,7 @@ function configureWindowsShell(): void { setShellIfWindows(); } catch (error) { const message = error instanceof Error ? error.message : String(error); - process.stderr.write(`deepcode: ${message}\n`); + writeStderrLine(`deepcode: ${message}\n`); process.exit(1); } } - -function readPackageInfo(): PackageInfo { - try { - const pkg = require("../package.json") as { name?: unknown; version?: unknown }; - return { - name: typeof pkg.name === "string" ? pkg.name : "@vegamo/deepcode-cli", - version: typeof pkg.version === "string" ? pkg.version : (CLI_VERSION ?? ""), - gitCommit: GIT_COMMIT_INFO ?? "", - }; - } catch { - return { name: "@vegamo/deepcode-cli", version: CLI_VERSION ?? "", gitCommit: GIT_COMMIT_INFO ?? "" }; - } -} diff --git a/packages/cli/src/common/update-check.ts b/packages/cli/src/common/update-check.ts index 3b82e51a..fb387fe3 100644 --- a/packages/cli/src/common/update-check.ts +++ b/packages/cli/src/common/update-check.ts @@ -6,6 +6,7 @@ import * as path from "path"; import { render, type Instance } from "ink"; import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; import { killProcessTree } from "@vegamo/deepcode-core"; +import type { PackageJson } from "../utils/package"; export type PackageInfo = { name: string; @@ -29,14 +30,14 @@ const MAX_NPM_VIEW_OUTPUT_CHARS = 64 * 1024; const TENCENT_MIRROR_REGISTRY = "https://mirrors.cloud.tencent.com/npm/"; export const UPDATE_SUCCESS_MESSAGE = "🎉 Update ran successfully! Please restart Deep Code."; -export async function promptForPendingUpdate(packageInfo: PackageInfo): Promise<{ installed: boolean }> { +export async function promptForPendingUpdate(packageInfo: PackageJson): Promise<{ installed: boolean }> { const state = readUpdateState(); const pending = state.pending; if (!pending) { return { installed: false }; } - if (compareVersions(packageInfo.version, pending.latestVersion) >= 0) { + if (compareVersions(packageInfo.version!, pending.latestVersion) >= 0) { writeUpdateState({ ...state, pending: null }); return { installed: false }; } @@ -49,7 +50,7 @@ export async function promptForPendingUpdate(packageInfo: PackageInfo): Promise< const installSpec = `${pending.packageName}@${pending.latestVersion}`; const installCommand = `npm install -g ${installSpec}`; const choice = await promptUpdateChoice({ - currentVersion: packageInfo.version, + currentVersion: packageInfo.version!, latestVersion: pending.latestVersion, installCommand, }); @@ -73,7 +74,7 @@ export async function promptForPendingUpdate(packageInfo: PackageInfo): Promise< return { installed: false }; } -export async function checkForNpmUpdate(packageInfo: PackageInfo): Promise { +export async function checkForNpmUpdate(packageInfo: PackageJson): Promise { if (!packageInfo.name || !packageInfo.version) { return; } diff --git a/packages/cli/src/tests/cli-args.test.ts b/packages/cli/src/tests/cli-args.test.ts index fd97de76..fe90eeed 100644 --- a/packages/cli/src/tests/cli-args.test.ts +++ b/packages/cli/src/tests/cli-args.test.ts @@ -1,47 +1,59 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { parseCliArgs } from "../cli-args"; +import { parseArguments, isValidSessionId } from "../cli-args"; -// ── parseCliArgs: basic parsing ────────────────────────────────────────────── +// ── isValidSessionId ───────────────────────────────────────────────────────── -test("parseCliArgs returns prompt after -p", () => { - const r = parseCliArgs(["-p", "hello world"]); +test("isValidSessionId accepts valid UUID", () => { + assert.ok(isValidSessionId("0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6")); +}); + +test("isValidSessionId rejects invalid format", () => { + assert.ok(!isValidSessionId("not-a-uuid")); + assert.ok(!isValidSessionId("")); + assert.ok(!isValidSessionId("abc")); +}); + +// ── parseArguments: basic parsing ────────────────────────────────────────────── + +test("parseArguments returns prompt after -p", async () => { + const r = await parseArguments(["-p", "hello world"]); assert.ok(!("message" in r)); assert.equal(r.prompt, "hello world"); }); -test("parseCliArgs returns prompt after --prompt", () => { - const r = parseCliArgs(["--prompt", "hello world"]); +test("parseArguments returns prompt after --prompt", async () => { + const r = await parseArguments(["--prompt", "hello world"]); assert.ok(!("message" in r)); assert.equal(r.prompt, "hello world"); }); -test("parseCliArgs returns undefined prompt when -p is not present", () => { - const r = parseCliArgs(["--resume"]); +test("parseArguments returns undefined prompt when -p is not present", async () => { + const r = await parseArguments(["--resume"]); assert.ok(!("message" in r)); assert.equal(r.prompt, undefined); }); -test("parseCliArgs returns session ID after --resume", () => { - const r = parseCliArgs(["--resume", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]); +test("parseArguments returns session ID after --resume", async () => { + const r = await parseArguments(["--resume", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]); assert.ok(!("message" in r)); assert.equal(r.resume, "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"); }); -test("parseCliArgs returns true when --resume has no value", () => { - const r = parseCliArgs(["--resume"]); +test("parseArguments returns true when --resume has no value", async () => { + const r = await parseArguments(["--resume"]); assert.ok(!("message" in r)); assert.equal(r.resume, true); }); -test("parseCliArgs returns undefined resume when not present", () => { - const r = parseCliArgs(["-p", "test"]); +test("parseArguments returns undefined resume when not present", async () => { + const r = await parseArguments(["-p", "test"]); assert.ok(!("message" in r)); assert.equal(r.resume, undefined); }); -test("parseCliArgs returns defaults for empty args", () => { - const r = parseCliArgs([]); +test("parseArguments returns defaults for empty args", async () => { + const r = await parseArguments([]); assert.ok(!("message" in r)); assert.equal(r.prompt, undefined); assert.equal(r.resume, undefined); @@ -49,120 +61,151 @@ test("parseCliArgs returns defaults for empty args", () => { assert.equal(r.help, false); }); -// ── parseCliArgs: -r alias ─────────────────────────────────────────────────── +// ── parseArguments: -r alias ─────────────────────────────────────────────────── -test("parseCliArgs returns session ID after -r", () => { - const r = parseCliArgs(["-r", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]); +test("parseArguments returns session ID after -r", async () => { + const r = await parseArguments(["-r", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]); assert.ok(!("message" in r)); assert.equal(r.resume, "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"); }); -test("parseCliArgs returns true when -r has no value", () => { - const r = parseCliArgs(["-r"]); +test("parseArguments returns true when -r has no value", async () => { + const r = await parseArguments(["-r"]); assert.ok(!("message" in r)); assert.equal(r.resume, true); }); -test("parseCliArgs handles -r combined with -p", () => { - const r = parseCliArgs(["-r", "session-123", "-p", "hello"]); +test("parseArguments handles -r combined with -p", async () => { + const r = await parseArguments(["-r", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6", "-p", "hello"]); assert.ok(!("message" in r)); - assert.equal(r.resume, "session-123"); + assert.equal(r.resume, "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"); assert.equal(r.prompt, "hello"); }); -test("parseCliArgs rejects bare -r with -p", () => { - const r = parseCliArgs(["-r", "-p", "hello"]); - assert.ok("message" in r); - assert.match(r.message, /Cannot use --resume/); -}); - -// ── parseCliArgs: --version / --help ───────────────────────────────────────── +// ── parseArguments: --version / --help ───────────────────────────────────────── -test("parseCliArgs detects --version", () => { - const r = parseCliArgs(["--version"]); +test("parseArguments detects --version", async () => { + const r = await parseArguments(["--version"]); assert.ok(!("message" in r)); assert.equal(r.version, true); assert.equal(r.help, false); }); -test("parseCliArgs detects -v", () => { - const r = parseCliArgs(["-v"]); +test("parseArguments detects -v", async () => { + const r = await parseArguments(["-v"]); assert.ok(!("message" in r)); assert.equal(r.version, true); }); -test("parseCliArgs detects --help", () => { - const r = parseCliArgs(["--help"]); +test("parseArguments detects --help", async () => { + const r = await parseArguments(["--help"]); assert.ok(!("message" in r)); assert.equal(r.help, true); assert.equal(r.version, false); }); -test("parseCliArgs detects -h", () => { - const r = parseCliArgs(["-h"]); +test("parseArguments detects -h", async () => { + const r = await parseArguments(["-h"]); assert.ok(!("message" in r)); assert.equal(r.help, true); }); -test("parseCliArgs version and help are false when not passed", () => { - const r = parseCliArgs(["-p", "hello"]); +test("parseArguments version and help are false when not passed", async () => { + const r = await parseArguments(["-p", "hello"]); assert.ok(!("message" in r)); assert.equal(r.version, false); assert.equal(r.help, false); }); -test("parseCliArgs handles -v combined with -r (both flags set)", () => { - const r = parseCliArgs(["-v", "-r", "abc"]); +test("parseArguments handles -v combined with -r (both flags set)", async () => { + const r = await parseArguments(["-v", "-r", "abc"]); assert.ok(!("message" in r)); assert.equal(r.version, true); assert.equal(r.resume, "abc"); }); -// ── parseCliArgs: combined usage ───────────────────────────────────────────── +// ── parseArguments: combined usage ───────────────────────────────────────────── -test("parseCliArgs handles --resume combined with -p", () => { - const r = parseCliArgs(["--resume", "session-123", "-p", "hello"]); +test("parseArguments handles --resume combined with -p", async () => { + const r = await parseArguments(["--resume", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6", "-p", "hello"]); assert.ok(!("message" in r)); - assert.equal(r.resume, "session-123"); + assert.equal(r.resume, "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"); assert.equal(r.prompt, "hello"); }); -test("parseCliArgs handles -p before --resume ", () => { - const r = parseCliArgs(["-p", "hello", "--resume", "session-123"]); +test("parseArguments handles -p before --resume ", async () => { + const r = await parseArguments(["-p", "hello", "--resume", "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"]); assert.ok(!("message" in r)); - assert.equal(r.resume, "session-123"); + assert.equal(r.resume, "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"); assert.equal(r.prompt, "hello"); }); -// ── parseCliArgs: validation ───────────────────────────────────────────────── - -test("parseCliArgs rejects bare --resume with -p", () => { - const r = parseCliArgs(["--resume", "-p", "hello"]); - assert.ok("message" in r); - assert.match(r.message, /Cannot use --resume/); -}); - -test("parseCliArgs rejects -p with bare --resume (reversed order)", () => { - const r = parseCliArgs(["-p", "hello", "--resume"]); - assert.ok("message" in r); - assert.match(r.message, /Cannot use --resume/); -}); - -test("parseCliArgs rejects unknown flags in strict mode", () => { - const r = parseCliArgs(["--unknown-flag"]); - assert.ok("message" in r); - assert.match(r.message, /Unknown argument/); -}); - -test("parseCliArgs rejects empty -p value", () => { - const r = parseCliArgs(["-p", ""]); - assert.ok("message" in r); - assert.match(r.message, /non-empty/); -}); - -test("parseCliArgs --version takes precedence over --help", () => { - const r = parseCliArgs(["--version", "--help"]); +test("parseArguments --version takes precedence over --help", async () => { + const r = await parseArguments(["--version", "--help"]); assert.ok(!("message" in r)); assert.equal(r.version, true); assert.equal(r.help, true); }); + +// ── parseArguments: error cases (mock process.exit) ──────────────────────────── +// Command-level and top-level errors both call process.exit(1) via yargs .fail(). + +function withMockedExit(fn: (exitSpy: { calls: number[] }) => Promise): Promise { + const original = process.exit; + const stderrWrite = process.stderr.write; + // Suppress yargs help/error output during tests + process.stderr.write = (() => true) as typeof process.stderr.write; + const exitSpy: { calls: number[] } = { calls: [] }; + process.exit = ((code?: number) => { + exitSpy.calls.push(code ?? 0); + throw new Error(`process.exit(${code})`); + }) as typeof process.exit; + return fn(exitSpy).finally(() => { + process.exit = original; + process.stderr.write = stderrWrite; + }); +} + +test("parseArguments exits on unknown flags", async () => { + await withMockedExit(async (exitSpy) => { + try { + await parseArguments(["--unknown-flag"]); + } catch { + /* expected */ + } + assert.ok(exitSpy.calls.length >= 1); + }); +}); + +test("parseArguments exits on bare -r with -p", async () => { + await withMockedExit(async (exitSpy) => { + try { + await parseArguments(["-r", "-p", "hello"]); + } catch { + /* expected */ + } + assert.ok(exitSpy.calls.length >= 1); + }); +}); + +test("parseArguments exits on empty -p value", async () => { + await withMockedExit(async (exitSpy) => { + try { + await parseArguments(["-p", ""]); + } catch { + /* expected */ + } + assert.ok(exitSpy.calls.length >= 1); + }); +}); + +test("parseArguments exits on invalid --resume session ID", async () => { + await withMockedExit(async (exitSpy) => { + try { + await parseArguments(["--resume", "not-a-uuid"]); + } catch { + /* expected */ + } + assert.ok(exitSpy.calls.length >= 1); + }); +}); diff --git a/packages/cli/src/utils/package.ts b/packages/cli/src/utils/package.ts new file mode 100644 index 00000000..1f195294 --- /dev/null +++ b/packages/cli/src/utils/package.ts @@ -0,0 +1,29 @@ +import { readPackageUp, type PackageJson as BasePackageJson } from "read-package-up"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { CLI_VERSION } from "../generated/git-commit"; + +export type PackageJson = BasePackageJson & { + config?: { + sandboxImageUri?: string; + }; +}; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +let packageJson: PackageJson; + +export async function getPackageJson(): Promise { + if (packageJson) { + return packageJson; + } + + const result = await readPackageUp({ cwd: __dirname }); + if (!result) { + return { name: "@vegamo/deepcode-cli", version: CLI_VERSION ?? "" }; + } + + packageJson = result.packageJson; + return packageJson; +} diff --git a/packages/cli/src/utils/stdioHelpers.ts b/packages/cli/src/utils/stdioHelpers.ts new file mode 100644 index 00000000..f0202e99 --- /dev/null +++ b/packages/cli/src/utils/stdioHelpers.ts @@ -0,0 +1,25 @@ +/** + * Writes a message to stdout with a trailing newline. + * Use for normal command output that the user expects to see. + * Avoids double newlines if the message already ends with one. + */ +export const writeStdoutLine = (message: string): void => { + process.stdout.write(message.endsWith("\n") ? message : `${message}\n`); +}; + +/** + * Writes a message to stderr with a trailing newline. + * Use for error messages in CLI commands. + * Avoids double newlines if the message already ends with one. + */ +export const writeStderrLine = (message: string): void => { + process.stderr.write(message.endsWith("\n") ? message : `${message}\n`); +}; + +/** + * Clears the terminal screen. + * Use instead of console.clear() to satisfy no-console lint rules. + */ +export const clearScreen = (): void => { + console.clear(); +}; diff --git a/packages/cli/src/utils/version.ts b/packages/cli/src/utils/version.ts new file mode 100644 index 00000000..f41a5c1f --- /dev/null +++ b/packages/cli/src/utils/version.ts @@ -0,0 +1,6 @@ +import { getPackageJson } from "./package.js"; + +export async function getCliVersion(): Promise { + const pkgJson = await getPackageJson(); + return process.env["CLI_VERSION"] || pkgJson?.version || "unknown"; +} From 43d0c112f43090b24b099eaf608eabb8ba3e10e7 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Thu, 25 Jun 2026 14:49:14 +0800 Subject: [PATCH 196/212] fix: update code examples in statusline documentation and improve module provider abort handling --- docs/statusline.md | 4 +- docs/statusline_en.md | 2 +- packages/cli/src/tests/statusline.test.ts | 36 ++++++++++++++ .../cli/src/ui/statusline/module-provider.ts | 47 +++++++++++-------- 4 files changed, 66 insertions(+), 23 deletions(-) diff --git a/docs/statusline.md b/docs/statusline.md index 4ab8a11d..4c731276 100644 --- a/docs/statusline.md +++ b/docs/statusline.md @@ -6,7 +6,7 @@ Deep Code CLI 支持通过插件向终端底部状态栏注入自定义信息( 在 `~/.deepcode/settings.json`(或项目级 `.deepcode/settings.json`)中添加 `statusline` 字段: -```jsonc +```json { "statusline": { "enabled": true, @@ -100,7 +100,7 @@ export default function tokensProvider({ projectRoot, session }) { ## 安全限制 -- **module provider 路径必须位于项目根目录或用户家目录之下**,绝对路径在这两个范围之外会被拒绝加载(防止从任意位置执行代码)。 +- **module provider 路径必须位于项目根目录或用户home目录之下**,绝对路径在这两个范围之外会被拒绝加载(防止从任意位置执行代码)。 - 单个 segment 文本被自动: - 取第一个非空行 - 去除 ANSI 转义序列 diff --git a/docs/statusline_en.md b/docs/statusline_en.md index bd14d91a..340c32cc 100644 --- a/docs/statusline_en.md +++ b/docs/statusline_en.md @@ -6,7 +6,7 @@ Deep Code CLI lets you inject custom information into the status line at the bot Add a `statusline` field to `~/.deepcode/settings.json` (or the project-level `.deepcode/settings.json`): -```jsonc +```json { "statusline": { "enabled": true, diff --git a/packages/cli/src/tests/statusline.test.ts b/packages/cli/src/tests/statusline.test.ts index 4417cca1..0336b626 100644 --- a/packages/cli/src/tests/statusline.test.ts +++ b/packages/cli/src/tests/statusline.test.ts @@ -171,6 +171,42 @@ test("loadModuleProvider succeeds for a well-formed module", async () => { } }); +test("loadModuleProvider removes abort listener after successful fetch", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-statusline-")); + const modPath = path.join(dir, "cleanup.mjs"); + fs.writeFileSync(modPath, "export default () => 'ok';", "utf8"); + try { + const provider = await loadModuleProvider(modPath, undefined, "cleanup", 10_000); + assert.ok(provider); + + const ac = new AbortController(); + const signal = ac.signal; + const originalAdd = signal.addEventListener; + const originalRemove = signal.removeEventListener; + let abortListenerAdds = 0; + let abortListenerRemoves = 0; + signal.addEventListener = function (this: AbortSignal, ...args: Parameters) { + if (args[0] === "abort") { + abortListenerAdds += 1; + } + return originalAdd.apply(this, args); + } as AbortSignal["addEventListener"]; + signal.removeEventListener = function (this: AbortSignal, ...args: Parameters) { + if (args[0] === "abort") { + abortListenerRemoves += 1; + } + return originalRemove.apply(this, args); + } as AbortSignal["removeEventListener"]; + + const result = await provider!.fetch({ projectRoot: dir, signal }); + assert.equal(result, "ok"); + assert.equal(abortListenerAdds, 1); + assert.equal(abortListenerRemoves, 1); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + test("resolveSettingsSources lets project-level providers override user-level by id", () => { const resolved = resolveSettingsSources( { diff --git a/packages/cli/src/ui/statusline/module-provider.ts b/packages/cli/src/ui/statusline/module-provider.ts index 0222bb6c..f45d6ab2 100644 --- a/packages/cli/src/ui/statusline/module-provider.ts +++ b/packages/cli/src/ui/statusline/module-provider.ts @@ -63,26 +63,33 @@ export async function loadModuleProvider( if (ctx.signal.aborted) { return ""; } - const result = await Promise.race([ - Promise.resolve().then(() => - providerFn({ - projectRoot: ctx.projectRoot, - session: ctx.getSessionInfo ? ctx.getSessionInfo() : null, - }) - ), - new Promise((_, reject) => { - const timer = setTimeout(() => reject(new Error("timeout")), timeout); - ctx.signal.addEventListener( - "abort", - () => { - clearTimeout(timer); - reject(new Error("aborted")); - }, - { once: true } - ); - }), - ]); - return typeof result === "string" ? result : ""; + let timer: ReturnType | null = null; + let onAbort: (() => void) | null = null; + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error("timeout")), timeout); + onAbort = () => reject(new Error("aborted")); + ctx.signal.addEventListener("abort", onAbort, { once: true }); + }); + + try { + const result = await Promise.race([ + Promise.resolve().then(() => + providerFn({ + projectRoot: ctx.projectRoot, + session: ctx.getSessionInfo ? ctx.getSessionInfo() : null, + }) + ), + timeoutPromise, + ]); + return typeof result === "string" ? result : ""; + } finally { + if (timer) { + clearTimeout(timer); + } + if (onAbort) { + ctx.signal.removeEventListener("abort", onAbort); + } + } }, }; } catch { From 12b2c09f9652dd1b9f80c7bdf520037650873692 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 25 Jun 2026 16:09:43 +0800 Subject: [PATCH 197/212] =?UTF-8?q?fix(cli):=20=E4=BF=AE=E5=A4=8D=20Window?= =?UTF-8?q?s=20Shell=20=E9=85=8D=E7=BD=AE=E6=97=B6=E7=9A=84=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E8=A1=8C=E5=8F=82=E6=95=B0=E5=A4=84=E7=90=86=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 Windows Shell 配置逻辑调整到 --version 和 --help 参数处理之后 - 避免在 Windows 无 Git Bash 环境下配置 Shell 时提前退出进程 - 确保命令行参数如 --version 和 --help 正常工作 - 添加 configureWindowsShell 函数注释说明其调用时机和作用 - 清理 cli.tsx 中的导入和调用顺序,提高代码可读性 --- packages/cli/src/cli.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index edf5fdbd..af33bc42 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -11,7 +11,6 @@ import { writeStderrLine, writeStdoutLine } from "./utils/stdioHelpers"; import { getPackageJson } from "./utils/package"; import { CLI_VERSION } from "./generated/git-commit"; -configureWindowsShell(); void main(); async function main(): Promise { @@ -24,6 +23,11 @@ async function main(): Promise { process.exit(0); } + // Configure Windows shell AFTER --version/--help handling. + // On Windows without Git Bash, setShellIfWindows() throws and calls process.exit(1). + // If called before argument parsing, --help and --version would fail on those machines. + configureWindowsShell(); + let initialPrompt = parsed.prompt; let resumeSessionId = parsed.resume; const projectRoot = process.cwd(); @@ -95,6 +99,12 @@ async function main(): Promise { startApp(); } +/** + * Configure shell environment for Windows. + * Sets NoDefaultCurrentDirectoryInExePath and resolves Git Bash path. + * Must be called after --version/--help handling to avoid blocking those + * commands on Windows machines without Git Bash installed. + */ function configureWindowsShell(): void { process.env.NoDefaultCurrentDirectoryInExePath = "1"; try { From 7c8ece94105b964b4dcc92b73942462062cfbe90 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 25 Jun 2026 16:22:29 +0800 Subject: [PATCH 198/212] =?UTF-8?q?refactor(cli):=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20PackageInfo=20=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 packages/cli/src/common/update-check.ts 文件中未使用的 PackageInfo 类型 - 减少代码冗余,提升代码可维护性 - 保持类型定义的简洁性与准确性 --- packages/cli/src/common/update-check.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/cli/src/common/update-check.ts b/packages/cli/src/common/update-check.ts index fb387fe3..2dad85f8 100644 --- a/packages/cli/src/common/update-check.ts +++ b/packages/cli/src/common/update-check.ts @@ -8,12 +8,6 @@ import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; import { killProcessTree } from "@vegamo/deepcode-core"; import type { PackageJson } from "../utils/package"; -export type PackageInfo = { - name: string; - version: string; - gitCommit?: string; -}; - type UpdateState = { pending?: { currentVersion: string; From 2fa60d54b78b9529f075c3ed8be2db0b6825ec9b Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 25 Jun 2026 18:02:43 +0800 Subject: [PATCH 199/212] =?UTF-8?q?style(MessageView):=20=E5=9C=A8?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=A7=86=E5=9B=BE=E7=BB=84=E4=BB=B6=E4=B8=AD?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B7=A6=E8=BE=B9=E8=B7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为外层Box组件增加marginLeft样式属性 - 保持其他样式和布局不变 - 修正UI布局中提示符内容的左侧间距问题 --- packages/cli/src/ui/components/MessageView/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/ui/components/MessageView/index.tsx b/packages/cli/src/ui/components/MessageView/index.tsx index 66df9625..a413f748 100644 --- a/packages/cli/src/ui/components/MessageView/index.tsx +++ b/packages/cli/src/ui/components/MessageView/index.tsx @@ -145,7 +145,7 @@ function PromptEchoLine({ }): React.ReactElement { const contentWidth = getPromptEchoContentWidth(width); return ( - + {"> "} From 545a4f54dd6bbb620c7f9d6cc907f17f659e5dc4 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 25 Jun 2026 18:09:58 +0800 Subject: [PATCH 200/212] =?UTF-8?q?test(cli):=20=E8=B0=83=E6=95=B4=20Messa?= =?UTF-8?q?geView=20=E7=BB=84=E4=BB=B6=E4=B8=AD=E7=9A=84=E7=BC=A9=E8=BF=9B?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改了消息渲染输出的缩进,从无空格缩进改为增加空格 - 确保多行消息内容对齐显示更美观 - 更新相关测试断言以匹配新的缩进格式 --- packages/cli/src/tests/message-view.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/tests/message-view.test.ts b/packages/cli/src/tests/message-view.test.ts index fbd2b097..abe95ef3 100644 --- a/packages/cli/src/tests/message-view.test.ts +++ b/packages/cli/src/tests/message-view.test.ts @@ -132,7 +132,7 @@ test("MessageView echoes submitted user prompts with live prompt wrapping width" const msg = makeSessionMessage({ role: "user", content: "abcdefg" }); const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 }); - assert.equal(stripAnsi(output), "> abcdef\n g\n"); + assert.equal(stripAnsi(output), " > abcdef\n g\n"); }); test("MessageView echoes model changes with submitted prompt wrapping", () => { @@ -143,7 +143,7 @@ test("MessageView echoes model changes with submitted prompt wrapping", () => { }); const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 }); - assert.equal(stripAnsi(output), "> abcdef\n gh\n"); + assert.equal(stripAnsi(output), " > abcdef\n gh\n"); }); test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => { From 7a447b80d86b7200bbf15003118e76363f2a1a47 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 25 Jun 2026 19:22:18 +0800 Subject: [PATCH 201/212] =?UTF-8?q?refactor(cli):=20=E7=BB=9F=E4=B8=80stdi?= =?UTF-8?q?o=E8=BE=85=E5=8A=A9=E5=87=BD=E6=95=B0=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将cli.tsx和cli-args.ts中stdioHelpers的导入路径调整为统一的stdio-helpers格式 - 规范了模块文件命名,提高代码一致性 - 未改动核心功能逻辑,仅更改导入路径字符串 --- packages/cli/src/cli-args.ts | 2 +- packages/cli/src/cli.tsx | 2 +- packages/cli/src/utils/{stdioHelpers.ts => stdio-helpers.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/cli/src/utils/{stdioHelpers.ts => stdio-helpers.ts} (100%) diff --git a/packages/cli/src/cli-args.ts b/packages/cli/src/cli-args.ts index 780869c0..b86eda41 100644 --- a/packages/cli/src/cli-args.ts +++ b/packages/cli/src/cli-args.ts @@ -6,7 +6,7 @@ import type { Argv } from "yargs"; import Yargs from "yargs"; import { getCliVersion } from "./utils/version"; -import { writeStderrLine } from "./utils/stdioHelpers"; +import { writeStderrLine } from "./utils/stdio-helpers"; import { hideBin } from "yargs/helpers"; // UUID v4 regex pattern for validation diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index af33bc42..80b11f08 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -7,7 +7,7 @@ import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check"; import { AppContainer } from "./ui"; import { parseArguments } from "./cli-args"; -import { writeStderrLine, writeStdoutLine } from "./utils/stdioHelpers"; +import { writeStderrLine, writeStdoutLine } from "./utils/stdio-helpers"; import { getPackageJson } from "./utils/package"; import { CLI_VERSION } from "./generated/git-commit"; diff --git a/packages/cli/src/utils/stdioHelpers.ts b/packages/cli/src/utils/stdio-helpers.ts similarity index 100% rename from packages/cli/src/utils/stdioHelpers.ts rename to packages/cli/src/utils/stdio-helpers.ts From 34ea71fd0d45064be8a0f82d1cec55db9455df68 Mon Sep 17 00:00:00 2001 From: hcyang Date: Thu, 25 Jun 2026 19:24:07 +0800 Subject: [PATCH 202/212] =?UTF-8?q?refactor(cli):=20=E7=BB=9F=E4=B8=80stdi?= =?UTF-8?q?o=E8=BE=85=E5=8A=A9=E5=87=BD=E6=95=B0=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将cli.tsx和cli-args.ts中stdioHelpers的导入路径调整为统一的stdio-helpers格式 - 规范了模块文件命名,提高代码一致性 - 未改动核心功能逻辑,仅更改导入路径字符串 --- packages/cli/src/utils/package.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/cli/src/utils/package.ts b/packages/cli/src/utils/package.ts index 1f195294..3c1401af 100644 --- a/packages/cli/src/utils/package.ts +++ b/packages/cli/src/utils/package.ts @@ -3,11 +3,7 @@ import { fileURLToPath } from "node:url"; import path from "node:path"; import { CLI_VERSION } from "../generated/git-commit"; -export type PackageJson = BasePackageJson & { - config?: { - sandboxImageUri?: string; - }; -}; +export type PackageJson = BasePackageJson; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); From da9f09950854151e7c27185392cc5dced00fe850 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 26 Jun 2026 10:06:56 +0800 Subject: [PATCH 203/212] feat: move the resume hint out of the exit summary box --- packages/cli/src/tests/exit-summary.test.ts | 23 +++++++--- packages/cli/src/ui/exit-summary.ts | 16 +++---- packages/cli/src/ui/index.ts | 2 +- packages/cli/src/ui/views/App.tsx | 51 +++++++++++++++------ packages/cli/src/ui/views/PromptInput.tsx | 7 +-- 5 files changed, 67 insertions(+), 32 deletions(-) diff --git a/packages/cli/src/tests/exit-summary.test.ts b/packages/cli/src/tests/exit-summary.test.ts index fd6b8ad0..45317b1e 100644 --- a/packages/cli/src/tests/exit-summary.test.ts +++ b/packages/cli/src/tests/exit-summary.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { buildExitSummaryText } from "../ui"; +import { buildExitSummaryText, buildResumeHintText } from "../ui"; import type { ModelUsage, SessionEntry } from "@vegamo/deepcode-core"; const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, ""); @@ -90,7 +90,7 @@ test("buildExitSummaryText does not derive usage rows from legacy aggregate usag assert.doesNotMatch(summary, /11,966/); }); -test("buildExitSummaryText shows resume hint when sessionId is provided", () => { +test("buildExitSummaryText does not show resume hint when sessionId is provided", () => { const sessionId = "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"; const summary = stripAnsi( buildExitSummaryText({ @@ -100,8 +100,8 @@ test("buildExitSummaryText shows resume hint when sessionId is provided", () => ); assert.match(summary, /Goodbye!/); - assert.match(summary, /deepcode --resume 0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6/); - assert.match(summary, /To continue this session/); + assert.doesNotMatch(summary, /deepcode --resume 0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6/); + assert.doesNotMatch(summary, /To continue this session/); }); test("buildExitSummaryText does not show resume hint when sessionId is omitted", () => { @@ -116,7 +116,7 @@ test("buildExitSummaryText does not show resume hint when sessionId is omitted", assert.doesNotMatch(summary, /To continue this session/); }); -test("buildExitSummaryText shows resume hint with null session", () => { +test("buildExitSummaryText does not show resume hint with null session", () => { const summary = stripAnsi( buildExitSummaryText({ session: null, @@ -125,7 +125,18 @@ test("buildExitSummaryText shows resume hint with null session", () => { ); assert.match(summary, /Goodbye!/); - assert.match(summary, /deepcode --resume test-session-id/); + assert.doesNotMatch(summary, /deepcode --resume test-session-id/); + assert.doesNotMatch(summary, /To continue this session/); +}); + +test("buildResumeHintText shows resume command when sessionId is provided", () => { + const hint = stripAnsi(buildResumeHintText("0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6") ?? ""); + + assert.equal(hint, "To continue this session, run deepcode --resume 0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6"); +}); + +test("buildResumeHintText returns null when sessionId is omitted", () => { + assert.equal(buildResumeHintText(), null); }); function buildSession(usage: ModelUsage | null, usagePerModel: Record | null = null): SessionEntry { diff --git a/packages/cli/src/ui/exit-summary.ts b/packages/cli/src/ui/exit-summary.ts index 1a28ab8f..67db1280 100644 --- a/packages/cli/src/ui/exit-summary.ts +++ b/packages/cli/src/ui/exit-summary.ts @@ -68,7 +68,7 @@ function extractUsageFields(usage: ModelUsage | null): UsageFields { } export function buildExitSummaryText(input: ExitSummaryInput): string { - const { session, sessionId } = input; + const { session } = input; const innerWidth = 98; const contentWidth = innerWidth - 4; // "│ " prefix + " │" suffix → 4 chars padding @@ -135,13 +135,6 @@ export function buildExitSummaryText(input: ExitSummaryInput): string { rows.push(""); - if (sessionId) { - const resumeHint = - chalk.dim(`To continue this session, run `) + chalk.hex("#229ac3")(`deepcode --resume ${sessionId}`); - rows.push(resumeHint); - rows.push(""); - } - const border = borderColor("─".repeat(innerWidth)); const top = `${borderColor("╭")}${border}${borderColor("╮")}`; const bottom = `${borderColor("╰")}${border}${borderColor("╯")}`; @@ -150,3 +143,10 @@ export function buildExitSummaryText(input: ExitSummaryInput): string { return [top, body, bottom].join("\n"); } + +export function buildResumeHintText(sessionId?: string): string | null { + if (!sessionId) { + return null; + } + return chalk.dim(`To continue this session, run `) + chalk.hex("#229ac3")(`deepcode --resume ${sessionId}`); +} diff --git a/packages/cli/src/ui/index.ts b/packages/cli/src/ui/index.ts index 8f155360..65415464 100644 --- a/packages/cli/src/ui/index.ts +++ b/packages/cli/src/ui/index.ts @@ -90,4 +90,4 @@ export { type FileMentionToken, } from "./core/file-mentions"; export { findExpandedThinkingId, isCollapsedThinking } from "./core/thinking-state"; -export { buildExitSummaryText } from "./exit-summary"; +export { buildExitSummaryText, buildResumeHintText } from "./exit-summary"; diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index 337832d5..456030c3 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -20,7 +20,7 @@ import { formatAskUserQuestionAnswers, } from "../core/ask-user-question"; import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt"; -import { buildExitSummaryText } from "../exit-summary"; +import { buildExitSummaryText, buildResumeHintText } from "../exit-summary"; import { RawMode, useRawModeContext } from "../contexts"; import { renderMessageToStdout } from "../components/MessageView/utils"; import { @@ -290,22 +290,40 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp }, [sessionManager]); writeRef.current = write; - const handlePrompt = useCallback( - async (submission: PromptSubmission) => { - if (submission.command === "exit") { - setIsExiting(true); - setTimeout(() => { - const activeSessionId = sessionManager.getActiveSessionId(); - const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null; - const summary = buildExitSummaryText({ session, sessionId: activeSessionId ?? undefined }); - process.stdout.write("\n"); + const handleExit = useCallback( + ({ showCommand, showSummary }: { showCommand: boolean; showSummary: boolean }) => { + setIsExiting(true); + setTimeout(() => { + const activeSessionId = sessionManager.getActiveSessionId(); + const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null; + const resumeHint = buildResumeHintText(activeSessionId ?? undefined); + + process.stdout.write("\n"); + if (showCommand) { process.stdout.write(chalk.rgb(34, 154, 195)("> /exit ")); process.stdout.write("\n\n"); + } + if (showSummary) { + const summary = buildExitSummaryText({ session, sessionId: activeSessionId ?? undefined }); process.stdout.write(summary); process.stdout.write("\n\n"); - sessionManager.dispose(); - exit(); - }, 0); + } + if (resumeHint) { + process.stdout.write(resumeHint); + process.stdout.write("\n"); + } + + sessionManager.dispose(); + exit(); + }, 0); + }, + [exit, sessionManager] + ); + + const handlePrompt = useCallback( + async (submission: PromptSubmission) => { + if (submission.command === "exit") { + handleExit({ showCommand: true, showSummary: true }); return; } if (submission.command === "new") { @@ -400,7 +418,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp [ sessionManager, pendingPermissionReply, - exit, + handleExit, onRestart, refreshSkills, refreshSessionsList, @@ -477,6 +495,10 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp [handlePrompt] ); + const handleExitShortcut = useCallback(() => { + handleExit({ showCommand: false, showSummary: false }); + }, [handleExit]); + const reloadActiveSessionView = useCallback( (sessionId: string): void => { resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true }); @@ -959,6 +981,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp onRawModeChange={handleRawModeChange} onInterrupt={handleInterrupt} onToggleProcessStdout={handleToggleProcessStdout} + onExitShortcut={handleExitShortcut} placeholder="Type your message..." statusLineSegments={statusLineSegments} statusLineSeparator={resolvedSettings.statusline.separator} diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index 2bf720b1..3f548def 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; -import { Box, Text, useApp, useStdout } from "ink"; +import { Box, Text, useStdout } from "ink"; import type { DOMElement } from "ink"; import chalk from "chalk"; import { ARGS_SEPARATOR } from "../constants"; @@ -101,6 +101,7 @@ type Props = { onRawModeChange?: (mode: string) => void; onInterrupt: () => void; onToggleProcessStdout?: () => void; + onExitShortcut?: () => void; }; const PROMPT_PREFIX_WIDTH = 2; @@ -132,9 +133,9 @@ export const PromptInput = React.memo(function PromptInput({ onModelConfigChange, onInterrupt, onToggleProcessStdout, + onExitShortcut, onRawModeChange, }: Props): React.ReactElement { - const { exit } = useApp(); const { stdout } = useStdout(); const inputTextRef = useRef(null); const [buffer, setBuffer] = useState(EMPTY_BUFFER); @@ -357,7 +358,7 @@ export const PromptInput = React.memo(function PromptInput({ } const now = Date.now(); if (pendingExit && now - lastCtrlDAt.current < 2000) { - exit(); + onExitShortcut?.(); return; } lastCtrlDAt.current = now; From 377e04161e925de4c3e7413946020e4dc48c4dcb Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 26 Jun 2026 10:36:58 +0800 Subject: [PATCH 204/212] fix: the new left margin need to be included in width calculations --- packages/cli/src/tests/message-view.test.ts | 20 ++++++++++++++++--- .../src/ui/components/MessageView/index.tsx | 6 ++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/tests/message-view.test.ts b/packages/cli/src/tests/message-view.test.ts index abe95ef3..c1c2d69d 100644 --- a/packages/cli/src/tests/message-view.test.ts +++ b/packages/cli/src/tests/message-view.test.ts @@ -127,12 +127,19 @@ test("renderMessageToStdout shows (no content) for empty user messages", () => { }); test("MessageView echoes submitted user prompts with live prompt wrapping width", () => { - assert.equal(getPromptEchoContentWidth(8), 6); + assert.equal(getPromptEchoContentWidth(8), 5); const msg = makeSessionMessage({ role: "user", content: "abcdefg" }); const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 }); - assert.equal(stripAnsi(output), " > abcdef\n g\n"); + const text = stripAnsi(output); + assert.equal(text, " > abcde\n fg\n"); + assert.ok( + text + .trimEnd() + .split("\n") + .every((line) => line.length <= 8) + ); }); test("MessageView echoes model changes with submitted prompt wrapping", () => { @@ -143,7 +150,14 @@ test("MessageView echoes model changes with submitted prompt wrapping", () => { }); const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 }); - assert.equal(stripAnsi(output), " > abcdef\n gh\n"); + const text = stripAnsi(output); + assert.equal(text, " > abcde\n fgh\n"); + assert.ok( + text + .trimEnd() + .split("\n") + .every((line) => line.length <= 8) + ); }); test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => { diff --git a/packages/cli/src/ui/components/MessageView/index.tsx b/packages/cli/src/ui/components/MessageView/index.tsx index a413f748..12b8229d 100644 --- a/packages/cli/src/ui/components/MessageView/index.tsx +++ b/packages/cli/src/ui/components/MessageView/index.tsx @@ -13,6 +13,7 @@ import type { DiffPreviewLine, MessageViewProps } from "./types"; import { RawMode, useRawModeContext } from "../../contexts"; const PROMPT_ECHO_PREFIX_WIDTH = 2; +const PROMPT_ECHO_MARGIN_LEFT = 1; export function MessageView({ message, collapsed, width = 80 }: MessageViewProps): React.ReactElement | null { const { mode } = useRawModeContext(); @@ -131,7 +132,7 @@ export function MessageView({ message, collapsed, width = 80 }: MessageViewProps } export function getPromptEchoContentWidth(width: number): number { - return Math.max(1, width - PROMPT_ECHO_PREFIX_WIDTH); + return Math.max(1, width - PROMPT_ECHO_MARGIN_LEFT - PROMPT_ECHO_PREFIX_WIDTH); } function PromptEchoLine({ @@ -144,8 +145,9 @@ function PromptEchoLine({ attachmentCount?: number; }): React.ReactElement { const contentWidth = getPromptEchoContentWidth(width); + const containerWidth = Math.max(1, width - PROMPT_ECHO_MARGIN_LEFT); return ( - + {"> "} From ada6ca74f293425621216986482b0b155ccaed01 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 26 Jun 2026 13:28:26 +0800 Subject: [PATCH 205/212] feat: update AGENTS.md --- .deepcode/AGENTS.md | 16 +++++++++++----- README.md | 12 ++++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.deepcode/AGENTS.md b/.deepcode/AGENTS.md index a1611307..7471cc15 100644 --- a/.deepcode/AGENTS.md +++ b/.deepcode/AGENTS.md @@ -14,12 +14,17 @@ packages/ │ ├── prompt.ts # System prompt builder & tool definitions │ └── settings.ts # Settings resolution from ~/.deepcode/settings.json ├── cli/src/ # Terminal UI (Ink/React) -│ ├── cli.tsx # Entry point — parses args (-p, -v), renders AppContainer -│ ├── ui/views/ # Top-level screens (App, PromptInput, SessionList, PermissionPrompt, etc.) +│ ├── cli.tsx # Entry point — renders AppContainer +│ ├── cli-args.ts # CLI argument parsing (yargs: -p, -r, -v, -h) +│ ├── common/ # Update checker +│ ├── utils/ # stdio helpers, version, package info +│ ├── generated/ # Build-time git commit info +│ ├── ui/views/ # Top-level screens (App, PromptInput, SessionList, PermissionPrompt, WelcomeScreen, UpdatePrompt, McpStatusList, etc.) │ ├── ui/components/ # Reusable Ink components (MessageView, DropdownMenu, ModelsDropdown, etc.) │ ├── ui/core/ # Prompt buffer, slash commands, file mentions, clipboard, undo/redo -│ ├── ui/hooks/ # Custom hooks (cursor, history navigation, paste handling, terminal input) +│ ├── ui/hooks/ # Custom hooks (cursor, history navigation, paste handling, terminal input, statusline) │ ├── ui/contexts/ # React contexts (AppContext, RawModeContext) +│ ├── ui/statusline/ # Pluggable statusline providers (command, module) │ └── tests/ # UI-focused tests with run-tests.mjs runner ├── vscode-ide-companion/ # VSCode extension companion │ └── src/ # extension.ts, provider.ts, utils.ts @@ -45,6 +50,7 @@ All commands run from the repo root. | `npm run check` | Runs typecheck + lint + format:check together | | `npm run build` | Orchestrates full build (scripts/build.js) — compiles core + bundles CLI + copies assets | | `npm run bundle` | Generates git commit info + esbuild bundle + copies bundled assets | +| `npm run build:vscode` | Builds the VSCode extension companion | | `npm test` | Runs all workspace tests (`npm run test --workspaces --if-present`) | | `npm run start` | Runs the locally built CLI (`scripts/start.js`) | | `npm run build-and-start` | Builds then starts the CLI | @@ -115,9 +121,9 @@ A **file history system** (`packages/core/src/common/file-history.ts`) provides **Slash commands**: `/skills`, `/model`, `/new`, `/init`, `/resume`, `/continue`, `/undo`, `/mcp`, `/raw`, `/exit`, plus dynamic `/skill-name` for each loaded skill. -**Key UI features**: `@` file mentions in the prompt input, `Ctrl+O` to view live process stdout, `Ctrl+V` to paste images, Shift+Enter for newlines, MCP server status display, undo selector, and permission prompts. +**Key UI features**: `@` file mentions in the prompt input, `Ctrl+O` to view live process stdout, `Ctrl+V` to paste images, `Ctrl+X` to clear images, Shift+Enter for newlines, pluggable statusline, MCP server status display, undo selector, and permission prompts. -**CLI flags**: `-p ` / `--prompt` to auto-submit a prompt on launch, `-v` / `--version`, `-h` / `--help`. +**CLI flags**: `-p ` / `--prompt` to auto-submit a prompt on launch, `-r [sessionId]` / `--resume [sessionId]` to resume a session or show the session picker, `-v` / `--version`, `-h` / `--help`. ## Agent-Specific Instructions diff --git a/README.md b/README.md index cde02314..39cd12bd 100644 --- a/README.md +++ b/README.md @@ -159,14 +159,18 @@ cd deepcode-cli # 安装依赖 npm install -# 本地开发(类型检查 + lint + 格式检查 + 构建) -npm run build - # 运行测试 npm test -# 链接到全局(即本地全局安装) +# CLI本地开发(类型检查 + lint + 格式检查 + 构建) +npm run build + +# CLI链接到全局(即本地全局安装) npm link + +# VSCode插件本地开发 +npm run build:vscode + ``` - 提交 PR 前请确保 `npm run check` 通过(类型检查 + lint + 格式检查) From 83c139efc8514578e089041b5319ff82afbaeb6a Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 26 Jun 2026 14:15:42 +0800 Subject: [PATCH 206/212] fix: initial-session Markdown rendering for VSCode extension --- packages/vscode-ide-companion/src/provider.ts | 10 +++++++--- .../vscode-ide-companion/src/tests/extension.test.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/vscode-ide-companion/src/provider.ts b/packages/vscode-ide-companion/src/provider.ts index 91aee0e4..2e3b4ac8 100644 --- a/packages/vscode-ide-companion/src/provider.ts +++ b/packages/vscode-ide-companion/src/provider.ts @@ -62,7 +62,7 @@ export async function handleWebviewMessage(message: unknown, deps: ProviderDeps) const msg = message as Record; if (msg.type === "ready") { - loadInitialSession(sessionManager, postMessage); + loadInitialSession(sessionManager, postMessage, renderMarkdown); await sendSkillsList(sessionManager, postMessage); return true; } @@ -138,7 +138,11 @@ export async function handleWebviewMessage(message: unknown, deps: ProviderDeps) return false; } -function loadInitialSession(sessionManager: ProviderDeps["sessionManager"], postMessage: PostMessageFn): void { +function loadInitialSession( + sessionManager: ProviderDeps["sessionManager"], + postMessage: PostMessageFn, + renderMarkdown: (text: string) => string +): void { const sessions = sessionManager.listSessions(); const sessionsList = toSessionList(sessions); @@ -152,7 +156,7 @@ function loadInitialSession(sessionManager: ProviderDeps["sessionManager"], post } const latestSession = sessions[0]; - loadSession(latestSession.id, sessionManager, postMessage, (t) => t); + loadSession(latestSession.id, sessionManager, postMessage, renderMarkdown); } export function loadSession( diff --git a/packages/vscode-ide-companion/src/tests/extension.test.ts b/packages/vscode-ide-companion/src/tests/extension.test.ts index 4f8d6e03..03f8e4e7 100644 --- a/packages/vscode-ide-companion/src/tests/extension.test.ts +++ b/packages/vscode-ide-companion/src/tests/extension.test.ts @@ -82,6 +82,18 @@ test("ready message triggers loadInitialSession and sendSkillsList", async () => assert.ok(types.includes("skillsList"), `Expected skillsList, got: ${types.join(", ")}`); }); +test("ready message renders markdown for initial session messages", async () => { + const deps = createDeps({ + messages: [{ role: "assistant", content: "**bold**", visible: true }], + }); + + await handleWebviewMessage({ type: "ready" }, deps); + + const loadMsg = deps.messages.find((m: any) => m.type === "loadSession") as any; + assert.ok(loadMsg, "Should send loadSession"); + assert.equal(loadMsg.messages[0].html, "

**bold**

"); +}); + test("ready with no sessions sends initializeEmpty", async () => { const deps = createDeps({ sessions: [] }); await handleWebviewMessage({ type: "ready" }, deps); From f2972d4547ab18788022ea5b8133878ba859b7c5 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 26 Jun 2026 14:27:20 +0800 Subject: [PATCH 207/212] chore: update package versions to 0.1.32 for deepcode-cli and deepcode-core, and 0.1.23 for deepcode-vscode --- package-lock.json | 6 +++--- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/vscode-ide-companion/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66cf2bce..00eda577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7705,7 +7705,7 @@ }, "packages/cli": { "name": "@vegamo/deepcode-cli", - "version": "0.1.31", + "version": "0.1.32", "license": "MIT", "dependencies": { "@vegamo/deepcode-core": "file:../core", @@ -7751,7 +7751,7 @@ }, "packages/core": { "name": "@vegamo/deepcode-core", - "version": "0.1.31", + "version": "0.1.32", "license": "MIT", "dependencies": { "chalk": "^5.6.2", @@ -7786,7 +7786,7 @@ }, "packages/vscode-ide-companion": { "name": "deepcode-vscode", - "version": "0.1.22", + "version": "0.1.23", "license": "MIT", "dependencies": { "@vegamo/deepcode-core": "file:../core", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1f657304..e3959fc4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.31", + "version": "0.1.32", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", diff --git a/packages/core/package.json b/packages/core/package.json index 1f924389..41c9da31 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-core", - "version": "0.1.31", + "version": "0.1.32", "description": "Deep Code core library — LLM session management, tool execution, and shared utilities", "license": "MIT", "type": "module", diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 6369f37b..fc7f8118 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -1,6 +1,6 @@ { "name": "deepcode-vscode", - "version": "0.1.22", + "version": "0.1.23", "publisher": "vegamo", "displayName": "Deep Code", "description": "Deep Code VSCode companion — AI-assisted development in your editor", From e47ef58cc94a155144fd1bd53f9eeddde9443df0 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 26 Jun 2026 14:53:49 +0800 Subject: [PATCH 208/212] chore: update package versions to 0.1.33 for deepcode-cli and deepcode-core, and enhance package validation in prepare-package script --- package-lock.json | 4 +- packages/cli/package.json | 4 +- packages/core/package.json | 2 +- scripts/prepare-package.js | 167 +++++++++++++++++++++++++++++++------ 4 files changed, 148 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00eda577..5c894763 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7705,7 +7705,7 @@ }, "packages/cli": { "name": "@vegamo/deepcode-cli", - "version": "0.1.32", + "version": "0.1.33", "license": "MIT", "dependencies": { "@vegamo/deepcode-core": "file:../core", @@ -7751,7 +7751,7 @@ }, "packages/core": { "name": "@vegamo/deepcode-core", - "version": "0.1.32", + "version": "0.1.33", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/packages/cli/package.json b/packages/cli/package.json index e3959fc4..c860e690 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-cli", - "version": "0.1.32", + "version": "0.1.33", "description": "Deep Code CLI - Vibe coding for the deepseek-v4 model in your terminal", "license": "MIT", "type": "module", @@ -15,6 +15,8 @@ "main": "./dist/cli.js", "files": [ "dist/cli.js", + "dist/chunks/**", + "dist/templates/**", "dist/bundled/**", "README.md", "LICENSE" diff --git a/packages/core/package.json b/packages/core/package.json index 41c9da31..bac0f126 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@vegamo/deepcode-core", - "version": "0.1.32", + "version": "0.1.33", "description": "Deep Code core library — LLM session management, tool execution, and shared utilities", "license": "MIT", "type": "module", diff --git a/scripts/prepare-package.js b/scripts/prepare-package.js index 02481c50..eb12dfea 100644 --- a/scripts/prepare-package.js +++ b/scripts/prepare-package.js @@ -27,18 +27,20 @@ function ok(msg) { function run(cmd, args, opts = {}) { const label = opts.label ?? `${cmd} ${args.join(" ")}`; - if (opts.dryRun) { + if (opts.dryRun && !opts.runInDryRun) { log(` (dry-run) ${label}`); return { status: 0, stdout: "" }; } const result = spawnSync(cmd, args, { stdio: opts.stdio ?? "inherit", cwd: opts.cwd ?? root, - shell: true, + shell: false, + encoding: opts.encoding, env: { ...process.env, ...opts.env }, }); if (result.status !== 0) { - fail(`Command failed: ${label}`); + const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); + fail(`Command failed: ${label}${output ? `\n${output}` : ""}`); } return result; } @@ -55,6 +57,70 @@ function isValidSemver(v) { return /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(v); } +function isValidNpmTag(v) { + return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(v); +} + +function hasPackFile(files, expectedPath) { + return files.includes(expectedPath); +} + +function hasPackPrefix(files, expectedPrefix) { + return files.some((file) => file.startsWith(expectedPrefix)); +} + +function validatePacklist(cwd, checks, opts = {}) { + const label = opts.label ?? `npm pack --dry-run --json --ignore-scripts`; + const result = run("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], { + cwd, + label, + stdio: "pipe", + encoding: "utf-8", + runInDryRun: true, + }); + const output = result.stdout.trim(); + const packs = JSON.parse(output); + const pack = Array.isArray(packs) ? packs[0] : packs; + const files = (pack?.files ?? []).map((file) => file.path); + const missing = []; + + for (const check of checks) { + const found = check.type === "prefix" ? hasPackPrefix(files, check.value) : hasPackFile(files, check.value); + if (!found) { + missing.push(check.label ?? check.value); + } + } + + if (missing.length > 0) { + fail(`Package tarball is missing required files:\n - ${missing.join("\n - ")}`); + } + + ok(`Validated package tarball (${files.length} files)`); +} + +function hasGitChanges(paths) { + const result = spawnSync("git", ["diff", "--quiet", "--", ...paths], { + cwd: root, + shell: false, + }); + if (result.status === 0) { + return false; + } + if (result.status === 1) { + return true; + } + fail("Unable to check release file changes."); +} + +function gitTagExists(tagName) { + const result = spawnSync("git", ["rev-parse", "-q", "--verify", `refs/tags/${tagName}`], { + cwd: root, + shell: false, + stdio: "ignore", + }); + return result.status === 0; +} + // ── Parse args ─────────────────────────────────────────────────────────────── const args = process.argv.slice(2); @@ -103,6 +169,10 @@ if (!isValidSemver(version)) { fail(`Invalid semver version: ${version}`); } +if (!isValidNpmTag(tag)) { + fail(`Invalid npm dist-tag: ${tag}`); +} + const TOTAL_STEPS = 8; // ── Banner ─────────────────────────────────────────────────────────────────── @@ -119,7 +189,7 @@ step(1, TOTAL_STEPS, "Checking git state..."); const gitStatus = spawnSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf-8", - shell: true, + shell: false, }); if (gitStatus.stdout.trim()) { fail("Working tree is not clean. Commit or stash changes first."); @@ -130,7 +200,7 @@ if (!force) { const gitBranch = spawnSync("git", ["branch", "--show-current"], { cwd: root, encoding: "utf-8", - shell: true, + shell: false, }); const branch = gitBranch.stdout.trim(); if (branch !== "main") { @@ -147,7 +217,7 @@ if (!dryRun) { const whoami = spawnSync("npm", ["whoami"], { cwd: root, encoding: "utf-8", - shell: true, + shell: false, }); if (whoami.status !== 0) { fail("Not logged in to npm. Run `npm login` first."); @@ -182,6 +252,12 @@ if (!dryRun) { log(` (dry-run) packages/cli: ${oldVersion} → ${version}`); } +run("npm", ["install", "--package-lock-only", "--ignore-scripts"], { + dryRun, + label: "npm install --package-lock-only --ignore-scripts", +}); +ok("package-lock.json is up to date"); + // ── 4. Quality checks ──────────────────────────────────────────────────────── step(4, TOTAL_STEPS, "Running quality checks (typecheck + lint + format)..."); @@ -209,6 +285,7 @@ const cliRoot = join(root, "packages", "cli"); const distDir = join(cliRoot, "dist"); const distCliJs = join(distDir, "cli.js"); const distChunks = join(distDir, "chunks"); +const distTemplates = join(distDir, "templates"); const distBundled = join(distDir, "bundled"); if (!existsSync(distCliJs)) { @@ -217,10 +294,24 @@ if (!existsSync(distCliJs)) { if (!existsSync(distChunks)) { fail(`Chunks directory not found: ${distChunks}. Run "npm run build" first.`); } +if (!existsSync(distTemplates)) { + fail(`Templates directory not found: ${distTemplates}. Run "npm run build" first.`); +} if (!existsSync(distBundled)) { fail(`Bundled assets not found: ${distBundled}. Run "npm run build" first.`); } +validatePacklist( + cliRoot, + [ + { type: "file", value: "dist/cli.js" }, + { type: "prefix", value: "dist/chunks/", label: "dist/chunks/*.js" }, + { type: "prefix", value: "dist/templates/", label: "dist/templates/**" }, + { type: "prefix", value: "dist/bundled/", label: "dist/bundled/**" }, + ], + { label: "cd packages/cli && npm pack --dry-run --json --ignore-scripts" } +); + // Copy README.md and LICENSE into dist/ for (const file of ["README.md", "LICENSE"]) { const src = join(root, file); @@ -258,9 +349,53 @@ if (!dryRun) { } log(" Written dist/package.json with dependencies: {}"); +if (!dryRun) { + validatePacklist( + distDir, + [ + { type: "file", value: "cli.js" }, + { type: "prefix", value: "chunks/", label: "chunks/*.js" }, + { type: "prefix", value: "templates/", label: "templates/**" }, + { type: "prefix", value: "bundled/", label: "bundled/**" }, + ], + { label: "cd dist && npm pack --dry-run --json --ignore-scripts" } + ); +} else { + log(" (dry-run) skipped dist/package.json tarball validation because dist/package.json was not written"); +} + ok("dist/ prepared for publishing"); -// ── 7. Publish from dist/ ──────────────────────────────────────────────────── +// ── Git commit + tag ───────────────────────────────────────────────────────── + +const releaseFiles = ["packages/core/package.json", "packages/cli/package.json", "package-lock.json"]; +const tagName = `v${version}`; + +if (!dryRun) { + log("\nCreating git commit and tag..."); + if (hasGitChanges(releaseFiles)) { + run("git", ["add", ...releaseFiles], { + label: "git add packages/*/package.json package-lock.json", + }); + run("git", ["commit", "-m", `chore(release): v${version}`], { + label: `git commit -m "chore(release): v${version}"`, + }); + } else { + log(" No release file changes to commit; tagging current HEAD"); + } + + if (gitTagExists(tagName)) { + fail(`Git tag already exists: ${tagName}`); + } + run("git", ["tag", tagName], { + label: `git tag ${tagName}`, + }); + ok(`Created tag ${tagName}`); +} else { + log("\n (dry-run) git add + commit + tag"); +} + +// ── 8. Publish from dist/ ──────────────────────────────────────────────────── step(8, TOTAL_STEPS, "Publishing @vegamo/deepcode-cli from dist/..."); @@ -274,24 +409,6 @@ run("npm", publishArgs, { }); ok(`Published @vegamo/deepcode-cli@${version}`); -// ── Git commit + tag ───────────────────────────────────────────────────────── - -if (!dryRun) { - log("\nCreating git commit and tag..."); - run("git", ["add", "packages/core/package.json", "packages/cli/package.json"], { - label: "git add packages/*/package.json", - }); - run("git", ["commit", "-m", `chore(release): v${version}`], { - label: `git commit -m "chore(release): v${version}"`, - }); - run("git", ["tag", `v${version}`], { - label: `git tag v${version}`, - }); - ok(`Created commit and tag v${version}`); -} else { - log("\n (dry-run) git add + commit + tag"); -} - // ── Done ───────────────────────────────────────────────────────────────────── console.log("\n========================================="); From c18d0d81cadda282677190ef31c80fa473522a8d Mon Sep 17 00:00:00 2001 From: hcyang Date: Fri, 26 Jun 2026 15:24:19 +0800 Subject: [PATCH 209/212] =?UTF-8?q?refactor(ui):=20=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E6=89=80=E6=9C=89=20process.stdout.write=20=E4=B8=BA=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E7=9A=84=E5=86=99=E5=85=A5=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 writeStdoutLine 函数替代重复的 process.stdout.write 调用 - 优化多处命令行输出,提升代码一致性和可维护性 - 修改退出提示、会话渲染及屏幕清理相关逻辑使用新函数 - 防止状态行在退出时显示,提升界面表现稳定性 --- packages/cli/src/ui/views/App.tsx | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index 456030c3..24788502 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -48,6 +48,7 @@ import type { } from "@vegamo/deepcode-core"; import { SessionManager } from "@vegamo/deepcode-core"; import { getCompactPromptTokenThreshold } from "@vegamo/deepcode-core"; +import { writeStdoutLine } from "../../utils/stdio-helpers"; type View = "chat" | "session-list" | "undo" | "mcp-status"; @@ -145,8 +146,8 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp onAssistantMessage: (message: SessionMessage) => { setMessages((prev) => [...prev, message]); if (rawModeRef.current === RawMode.Raw) { - process.stdout.write("\n"); - process.stdout.write(renderMessageToStdout(message, rawModeRef.current) + "\n\n"); + writeStdoutLine("\n"); + writeStdoutLine(renderMessageToStdout(message, rawModeRef.current) + "\n\n"); } }, onSessionEntryUpdated: (entry) => { @@ -196,7 +197,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp const resetStaticView = useCallback( (loadedMessages: SessionMessage[], options?: { clearScreen?: boolean }): Promise => { if (options?.clearScreen) { - process.stdout.write(ANSI_CLEAR_SCREEN); + writeStdoutLine(ANSI_CLEAR_SCREEN); } setMessages([]); setWelcomeNonce((n) => n + 1); @@ -298,19 +299,19 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null; const resumeHint = buildResumeHintText(activeSessionId ?? undefined); - process.stdout.write("\n"); + writeStdoutLine("\n"); if (showCommand) { - process.stdout.write(chalk.rgb(34, 154, 195)("> /exit ")); - process.stdout.write("\n\n"); + writeStdoutLine(chalk.rgb(34, 154, 195)(" > /exit ")); + writeStdoutLine("\n"); } if (showSummary) { const summary = buildExitSummaryText({ session, sessionId: activeSessionId ?? undefined }); - process.stdout.write(summary); - process.stdout.write("\n\n"); + writeStdoutLine(summary); + writeStdoutLine("\n"); } if (resumeHint) { - process.stdout.write(resumeHint); - process.stdout.write("\n"); + writeStdoutLine(resumeHint); + writeStdoutLine("\n"); } sessionManager.dispose(); @@ -628,7 +629,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp setShowWelcome(false); setMessages([]); // Clear screen to remove stale formatted text. - process.stdout.write(ANSI_CLEAR_SCREEN); + writeStdoutLine(ANSI_CLEAR_SCREEN); setTimeout(() => { if (nextMode === RawMode.Raw) { @@ -667,7 +668,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp if (mode === RawMode.Raw) { // In raw mode, re-render all messages directly to stdout at the new width. // Use process.stdout.write instead of writeRef to avoid Ink interference. - process.stdout.write(ANSI_CLEAR_SCREEN); + writeStdoutLine(ANSI_CLEAR_SCREEN); const activeSessionId = sessionManager.getActiveSessionId(); const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : []; renderRawModeMessages(allMessages, mode); @@ -898,7 +899,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp ); }}
- {busy || statusLine ? : null} + {(busy || statusLine) && !isExiting ? : null} {errorLine ? ( Error: {errorLine} From 47d1f03fafce9901142c27a0e7da699dea0e55ef Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Fri, 26 Jun 2026 17:22:42 +0800 Subject: [PATCH 210/212] feat: enhance prompt attachment manager to support multiple image attachments and improve preview handling --- .../resources/prompt-attachments.js | 72 ++++-- .../resources/webview.css | 1 + .../src/tests/extension.test.ts | 20 ++ .../src/tests/prompt-attachments.test.ts | 240 ++++++++++++++++++ 4 files changed, 307 insertions(+), 26 deletions(-) create mode 100644 packages/vscode-ide-companion/src/tests/prompt-attachments.test.ts diff --git a/packages/vscode-ide-companion/resources/prompt-attachments.js b/packages/vscode-ide-companion/resources/prompt-attachments.js index 81bfc174..e6c628ac 100644 --- a/packages/vscode-ide-companion/resources/prompt-attachments.js +++ b/packages/vscode-ide-companion/resources/prompt-attachments.js @@ -48,9 +48,11 @@ throw new Error("Prompt attachment manager requires promptInput, inputWrap, and toolsLine."); } - let attachment = null; + let attachments = []; + let nextAttachmentId = 0; let previewPopup = null; let previewImage = null; + let previewAnchor = null; function ensurePreviewPopup() { if (previewPopup) { @@ -68,6 +70,7 @@ if (!previewPopup) { return; } + previewAnchor = null; previewPopup.classList.remove("show"); } @@ -101,12 +104,13 @@ previewPopup.style.top = top + "px"; } - function showPreview(anchor) { + function showPreview(anchor, attachment) { if (!attachment) { return; } ensurePreviewPopup(); + previewAnchor = anchor; previewImage.src = attachment.dataUrl; previewPopup.classList.add("show"); updatePreviewPosition(anchor); @@ -114,24 +118,35 @@ function emitChange() { onAttachmentChange({ - hasAttachments: Boolean(attachment), - attachments: attachment ? [attachment] : [], + hasAttachments: attachments.length > 0, + attachments: attachments.slice(), }); } function clear() { - attachment = null; + attachments = []; toolsLine.innerHTML = ""; toolsLine.classList.remove("has-attachment"); hidePreview(); emitChange(); } - function createAttachmentNode() { + function removeAttachment(id) { + const nextAttachments = attachments.filter((attachment) => attachment.id !== id); + if (nextAttachments.length === attachments.length) { + return; + } + attachments = nextAttachments; + render(); + emitChange(); + } + + function createAttachmentNode(attachment) { const wrapper = createElement("div", "chat-attached-context-attachment show-file-icons"); wrapper.tabIndex = 0; wrapper.setAttribute("role", "button"); wrapper.setAttribute("aria-label", ATTACHMENT_LABEL + " (删除)"); + wrapper.dataset.attachmentId = String(attachment.id); wrapper.draggable = true; const removeButton = createElement("a", "monaco-button codicon codicon-close"); @@ -143,7 +158,7 @@ removeButton.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); - clear(); + removeAttachment(attachment.id); }); const iconLabel = createElement("div", "monaco-icon-label"); @@ -166,7 +181,7 @@ wrapper.appendChild(pill); wrapper.appendChild(text); - const show = () => showPreview(wrapper); + const show = () => showPreview(wrapper, attachment); wrapper.addEventListener("mouseenter", show); wrapper.addEventListener("focus", show); wrapper.addEventListener("mouseleave", hidePreview); @@ -177,7 +192,7 @@ wrapper.addEventListener("keydown", (event) => { if (event.key === "Delete" || event.key === "Backspace") { event.preventDefault(); - clear(); + removeAttachment(attachment.id); } }); @@ -186,37 +201,44 @@ function render() { toolsLine.innerHTML = ""; - toolsLine.classList.toggle("has-attachment", Boolean(attachment)); - if (!attachment) { + toolsLine.classList.toggle("has-attachment", attachments.length > 0); + if (attachments.length === 0) { hidePreview(); return; } - toolsLine.appendChild(createAttachmentNode()); + for (const attachment of attachments) { + toolsLine.appendChild(createAttachmentNode(attachment)); + } + if (previewAnchor && !toolsLine.contains(previewAnchor)) { + hidePreview(); + } } - function setAttachmentData(data) { + function addAttachmentData(data) { if (!data?.dataUrl) { return false; } - attachment = { + nextAttachmentId += 1; + attachments.push({ + id: nextAttachmentId, name: data.name || ATTACHMENT_LABEL, mimeType: data.mimeType || "image/png", dataUrl: data.dataUrl, label: ATTACHMENT_LABEL, - }; + }); render(); emitChange(); return true; } - async function setAttachmentFromFile(file) { + async function addAttachmentFromFile(file) { if (!isImageFile(file)) { return false; } const dataUrl = await readFileAsDataUrl(file); - return setAttachmentData({ + return addAttachmentData({ name: file.name || ATTACHMENT_LABEL, mimeType: file.type || "image/png", dataUrl, @@ -232,7 +254,7 @@ event.preventDefault(); try { - await setAttachmentFromFile(file); + await addAttachmentFromFile(file); } catch (error) { console.error("Failed to attach pasted image.", error); } @@ -241,18 +263,16 @@ promptInput.addEventListener("paste", handlePaste); window.addEventListener("resize", () => { - const attachmentNode = toolsLine.querySelector(".chat-attached-context-attachment"); - if (previewPopup?.classList.contains("show") && attachmentNode) { - updatePreviewPosition(attachmentNode); + if (previewPopup?.classList.contains("show") && previewAnchor) { + updatePreviewPosition(previewAnchor); } }); window.addEventListener( "scroll", () => { - const attachmentNode = toolsLine.querySelector(".chat-attached-context-attachment"); - if (previewPopup?.classList.contains("show") && attachmentNode) { - updatePreviewPosition(attachmentNode); + if (previewPopup?.classList.contains("show") && previewAnchor) { + updatePreviewPosition(previewAnchor); } }, true @@ -261,10 +281,10 @@ return { clear, hasAttachments() { - return Boolean(attachment); + return attachments.length > 0; }, getImageUrls() { - return attachment ? [attachment.dataUrl] : []; + return attachments.map((attachment) => attachment.dataUrl); }, }; } diff --git a/packages/vscode-ide-companion/resources/webview.css b/packages/vscode-ide-companion/resources/webview.css index ea1d71a2..98ffa62e 100644 --- a/packages/vscode-ide-companion/resources/webview.css +++ b/packages/vscode-ide-companion/resources/webview.css @@ -1121,6 +1121,7 @@ body { .tools-line { display: none; align-items: center; + flex-wrap: wrap; gap: 8px; min-height: 0; padding: 0 12px; diff --git a/packages/vscode-ide-companion/src/tests/extension.test.ts b/packages/vscode-ide-companion/src/tests/extension.test.ts index 03f8e4e7..9ea39656 100644 --- a/packages/vscode-ide-companion/src/tests/extension.test.ts +++ b/packages/vscode-ide-companion/src/tests/extension.test.ts @@ -267,6 +267,26 @@ test("userPrompt with images sends userMessage with image placeholder", async () assert.equal((userMsg as any).content, "粘贴的图像"); }); +test("userPrompt passes multiple image urls to the session manager", async () => { + const deps = createDeps(); + let submittedPrompt: any = null; + (deps.sessionManager as any).handleUserPrompt = (prompt: any) => { + submittedPrompt = prompt; + return Promise.resolve(); + }; + + await handleWebviewMessage( + { + type: "userPrompt", + prompt: "", + images: ["data:image/png;base64,abc", "data:image/jpeg;base64,def"], + }, + deps + ); + + assert.deepEqual(submittedPrompt?.imageUrls, ["data:image/png;base64,abc", "data:image/jpeg;base64,def"]); +}); + test("userPrompt with permissions (continue) does not send userMessage", async () => { const deps = createDeps(); await handleWebviewMessage( diff --git a/packages/vscode-ide-companion/src/tests/prompt-attachments.test.ts b/packages/vscode-ide-companion/src/tests/prompt-attachments.test.ts new file mode 100644 index 00000000..314826e3 --- /dev/null +++ b/packages/vscode-ide-companion/src/tests/prompt-attachments.test.ts @@ -0,0 +1,240 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import vm from "node:vm"; +import { fileURLToPath } from "node:url"; + +type EventHandler = (event: any) => unknown; + +class FakeClassList { + private readonly classes = new Set(); + + constructor(className = "") { + for (const classPart of className.split(/\s+/)) { + if (classPart) { + this.classes.add(classPart); + } + } + } + + add(className: string): void { + this.classes.add(className); + } + + remove(className: string): void { + this.classes.delete(className); + } + + contains(className: string): boolean { + return this.classes.has(className); + } + + toggle(className: string, force?: boolean): boolean { + const shouldAdd = force ?? !this.classes.has(className); + if (shouldAdd) { + this.classes.add(className); + } else { + this.classes.delete(className); + } + return shouldAdd; + } +} + +class FakeElement { + readonly tagName: string; + className = ""; + classList = new FakeClassList(); + children: FakeElement[] = []; + parent: FakeElement | null = null; + dataset: Record = {}; + style: Record = {}; + textContent = ""; + tabIndex = 0; + draggable = false; + href = ""; + src = ""; + alt = ""; + private readonly attributes = new Map(); + private readonly listeners = new Map(); + + constructor(tagName: string) { + this.tagName = tagName; + } + + setAttribute(name: string, value: string): void { + this.attributes.set(name, value); + } + + appendChild(child: FakeElement): FakeElement { + child.parent = this; + this.children.push(child); + return child; + } + + set innerHTML(_value: string) { + for (const child of this.children) { + child.parent = null; + } + this.children = []; + } + + get innerHTML(): string { + return ""; + } + + addEventListener(type: string, handler: EventHandler): void { + const listeners = this.listeners.get(type) ?? []; + listeners.push(handler); + this.listeners.set(type, listeners); + } + + async dispatchEvent(event: any): Promise { + event.type ??= ""; + for (const handler of this.listeners.get(event.type) ?? []) { + await handler(event); + } + } + + contains(candidate: FakeElement | null): boolean { + if (!candidate) { + return false; + } + if (candidate === this) { + return true; + } + return this.children.some((child) => child.contains(candidate)); + } + + querySelector(selector: string): FakeElement | null { + if (!selector.startsWith(".")) { + return null; + } + const className = selector.slice(1); + for (const child of this.children) { + if (child.className.split(/\s+/).includes(className)) { + return child; + } + const match = child.querySelector(selector); + if (match) { + return match; + } + } + return null; + } + + getBoundingClientRect(): { left: number; top: number; bottom: number; width: number; height: number } { + return { left: 20, top: 80, bottom: 100, width: 160, height: 40 }; + } +} + +class FakeDocument { + readonly body = new FakeElement("body"); + + createElement(tagName: string): FakeElement { + return new FakeElement(tagName); + } +} + +class FakeFileReader { + result: string | null = null; + onload: (() => void) | null = null; + onerror: (() => void) | null = null; + error: Error | null = null; + + readAsDataURL(file: { dataUrl?: string }): void { + this.result = file.dataUrl ?? ""; + this.onload?.(); + } +} + +function loadAttachmentManager(): { + manager: { clear: () => void; hasAttachments: () => boolean; getImageUrls: () => string[] }; + promptInput: FakeElement; + toolsLine: FakeElement; +} { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const scriptPath = path.resolve(__dirname, "../../resources/prompt-attachments.js"); + const script = fs.readFileSync(scriptPath, "utf8"); + + const document = new FakeDocument(); + const window = { + innerWidth: 1024, + innerHeight: 768, + addEventListener: () => {}, + createPromptAttachmentManager: undefined as + | undefined + | ((options: Record) => { + clear: () => void; + hasAttachments: () => boolean; + getImageUrls: () => string[]; + }), + }; + + vm.runInNewContext(script, { console, document, window, FileReader: FakeFileReader }); + + const createPromptAttachmentManager = window.createPromptAttachmentManager; + if (typeof createPromptAttachmentManager !== "function") { + throw new Error("Prompt attachment manager was not registered."); + } + const promptInput = new FakeElement("textarea"); + const inputWrap = new FakeElement("div"); + const toolsLine = new FakeElement("div"); + const manager = createPromptAttachmentManager({ promptInput, inputWrap, toolsLine }); + + return { manager, promptInput, toolsLine }; +} + +async function pasteImage(promptInput: FakeElement, dataUrl: string): Promise { + let defaultPrevented = false; + await promptInput.dispatchEvent({ + type: "paste", + clipboardData: { + items: [ + { + kind: "file", + getAsFile: () => ({ type: "image/png", name: "image.png", dataUrl }), + }, + ], + }, + preventDefault: () => { + defaultPrevented = true; + }, + }); + assert.equal(defaultPrevented, true); +} + +test("prompt attachment manager appends pasted images instead of replacing the previous image", async () => { + const { manager, promptInput, toolsLine } = loadAttachmentManager(); + + await pasteImage(promptInput, "data:image/png;base64,first"); + await pasteImage(promptInput, "data:image/png;base64,second"); + + assert.equal(manager.hasAttachments(), true); + assert.deepEqual(Array.from(manager.getImageUrls()), ["data:image/png;base64,first", "data:image/png;base64,second"]); + assert.equal(toolsLine.children.length, 2); + assert.equal(toolsLine.classList.contains("has-attachment"), true); +}); + +test("prompt attachment manager removes one pasted image without clearing the rest", async () => { + const { manager, promptInput, toolsLine } = loadAttachmentManager(); + + await pasteImage(promptInput, "data:image/png;base64,first"); + await pasteImage(promptInput, "data:image/png;base64,second"); + + const firstAttachment = toolsLine.children[0]; + const removeButton = firstAttachment.children[0]; + await removeButton.dispatchEvent({ + type: "click", + preventDefault: () => {}, + stopPropagation: () => {}, + }); + + assert.deepEqual(Array.from(manager.getImageUrls()), ["data:image/png;base64,second"]); + assert.equal(toolsLine.children.length, 1); + + manager.clear(); + assert.equal(manager.hasAttachments(), false); + assert.deepEqual(Array.from(manager.getImageUrls()), []); + assert.equal(toolsLine.children.length, 0); +}); From 888a20ac01116ccf32f76bee4782bac916cc0dd3 Mon Sep 17 00:00:00 2001 From: Ji Zhang Date: Mon, 29 Jun 2026 11:48:22 +0800 Subject: [PATCH 211/212] feat: added writeStdout() for exact stdout writes with no trailing newline. --- packages/cli/src/ui/views/App.tsx | 10 +++++----- packages/cli/src/utils/stdio-helpers.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index 24788502..3b2886cd 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -48,7 +48,7 @@ import type { } from "@vegamo/deepcode-core"; import { SessionManager } from "@vegamo/deepcode-core"; import { getCompactPromptTokenThreshold } from "@vegamo/deepcode-core"; -import { writeStdoutLine } from "../../utils/stdio-helpers"; +import { writeStdout, writeStdoutLine } from "../../utils/stdio-helpers"; type View = "chat" | "session-list" | "undo" | "mcp-status"; @@ -197,7 +197,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp const resetStaticView = useCallback( (loadedMessages: SessionMessage[], options?: { clearScreen?: boolean }): Promise => { if (options?.clearScreen) { - writeStdoutLine(ANSI_CLEAR_SCREEN); + writeStdout(ANSI_CLEAR_SCREEN); } setMessages([]); setWelcomeNonce((n) => n + 1); @@ -629,7 +629,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp setShowWelcome(false); setMessages([]); // Clear screen to remove stale formatted text. - writeStdoutLine(ANSI_CLEAR_SCREEN); + writeStdout(ANSI_CLEAR_SCREEN); setTimeout(() => { if (nextMode === RawMode.Raw) { @@ -667,8 +667,8 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp if (mode === RawMode.Raw) { // In raw mode, re-render all messages directly to stdout at the new width. - // Use process.stdout.write instead of writeRef to avoid Ink interference. - writeStdoutLine(ANSI_CLEAR_SCREEN); + // Use direct stdout instead of writeRef to avoid Ink interference. + writeStdout(ANSI_CLEAR_SCREEN); const activeSessionId = sessionManager.getActiveSessionId(); const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : []; renderRawModeMessages(allMessages, mode); diff --git a/packages/cli/src/utils/stdio-helpers.ts b/packages/cli/src/utils/stdio-helpers.ts index f0202e99..3f117267 100644 --- a/packages/cli/src/utils/stdio-helpers.ts +++ b/packages/cli/src/utils/stdio-helpers.ts @@ -1,3 +1,11 @@ +/** + * Writes a message to stdout exactly as provided. + * Use for terminal control sequences or output that manages its own spacing. + */ +export const writeStdout = (message: string): void => { + process.stdout.write(message); +}; + /** * Writes a message to stdout with a trailing newline. * Use for normal command output that the user expects to see. From 8625cb3a055129f338d32f740c1ab1afc7ebeeea Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 29 Jun 2026 14:03:15 +0800 Subject: [PATCH 212/212] =?UTF-8?q?style(cli):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E6=8F=8F=E8=BF=B0=E7=9A=84=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E9=97=B4=E8=B7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在选项描述外围添加了左侧外边距的容器Box - 改进了文本显示的缩进和视觉层次感 - 保持了原有的文本颜色和显示逻辑不变 - 提升了用户界面的一致性和可读性 --- packages/cli/src/ui/views/AskUserQuestionPrompt.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx b/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx index a2f91adb..ccce5f7e 100644 --- a/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx +++ b/packages/cli/src/ui/views/AskUserQuestionPrompt.tsx @@ -206,7 +206,11 @@ export function AskUserQuestionPrompt({ questions, onSubmit, onCancel }: Props): )} ) : null} - {option.description ? {option.description} : null} + {option.description ? ( + + {option.description} + + ) : null} ); })}