Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.12.4

- Repomix health and packing launch an installed JavaScript bin directly with the current Node executable and separate arguments, eliminating the cmd/npx shell chain. Local package bin discovery and explicit absolute customCliPath are supported; shell wrappers and npx caches are no longer invoked. Missing or invalid entries use the builtin packer without installing anything.
- Real child-process regression fixtures cover Unicode, spaces and shell metacharacters in paths/arguments, missing or invalid overrides, local package discovery, timeout and cancellation with PID exit checks. These validate the launcher contract, not an installed upstream Repomix release.
- main protection now requires pull requests, the Node 22/24 regression matrix and all three CodeQL analysis checks, including administrators; force pushes and branch deletion are disabled. Independent approval is not required by GitHub for this single-maintainer workflow.

## 0.12.3

- Add an opt-in six-task TavernDesk acceptance entry point. Discover candidate files with bounded native searches, locate unique live navigation controls, and verify command assignments and method declaration snippets against current source hashes.
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ For a version change, align `package.json`, the root entries in `package-lock.js

Before merging a topic branch, record the problem, changes, failures and actual verification in the existing work log. Require successful Node 22/24 and CodeQL results for the exact PR head. Keep independent review and author self-review distinct, and identify unverified integrations. Repository branch-protection settings require a separate owner decision; this document does not establish enforced protection. A successful CodeQL run also does not prove existing alerts are closed.

On 2026-09-08, the owner authorized applying main protection. The read-back confirmed required pull requests, strict Node 22/24 regression and three CodeQL analysis checks bound to GitHub Actions, enforcement for administrators, resolved conversations, and disabled force pushes/deletions. The single-maintainer policy requires zero GitHub approvals; this does not constitute an independent review. Re-read GitHub settings when verifying current enforcement.

SDK policy follows [Microsoft global.json guidance](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json); dependency locking uses [NuGet locked restore](https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies).
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ WinCode is a local MCP server built for Windows and .NET engineering. It bridges
- **Inspect the running app:** Enumerate visible windows, query specific controls or subtrees, and capture numbered visual overlays without activating or stealing focus from the target.
- **Review with evidence:** Trace on-screen widgets back to literal XAML declaration tags, line numbers, and file hashes, with transparent reporting for ambiguity, truncation, or degraded upstreams.

Current source version: **0.12.3**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history.
Current source version: **0.12.4**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history.

### Quick start

Expand Down Expand Up @@ -211,7 +211,7 @@ WinCode 是面向 Windows 与 .NET 工程研发的本地 MCP 服务。它将项
- **观察实际界面:**发现系统可见窗口,按条件定向查询目标控件或子树,并在不激活、不抢占前台焦点的前提下获取数字标注截图。
- **源码双向印证:**将运行时抓取的控件关联回 XAML 源码声明的起始行号、代码片段与文件哈希,清晰报告歧义、截断与降级状态。

当前源码版本为 **0.12.3**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。
当前源码版本为 **0.12.4**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。

### 快速上手

Expand Down
11 changes: 11 additions & 0 deletions docs/codex_worklog.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,3 +484,14 @@
- 临时探针曾调用不存在的 connectTransport(失败日志 wp5-user-card-probe.log),核对既有 runtime-contract 测试后改为公开 SDK InMemoryTransport 与 Gateway 内已有 server.connect,复测通过;没有为测试添加生产连接 API。头像初检把相对路径误按 WinCode cwd 解析,随后查明 AppDataPaths 的 data 根规则并正确复核;初检 false 不属于应用文件丢失。
- 为让远端检查报告直接提供实际测试数量,check 使用 Node 内置 TAP reporter 并收集 tests/pass/fail/cancelled/skipped 摘要;不只留下“命令退出 0”。缺少完整测试摘要仍报错,不把未运行套件记为通过。
- TAP 报告补充实测:核心 check 通过,report.tests={tests:301,pass:300,fail:0,cancelled:0,skipped:1}(2026-09-08T13-05-15-874Z-core);桌面 check 通过,report.tests={tests:35,pass:35,fail:0,cancelled:0,skipped:0}(2026-09-08T13-06-36-861Z-desktop)。更新 PR 后重新核对精确 head CI,不沿用先前 head 的成功状态。

## 2026-09-08 21:37 — 0.12.4 安全启动与剩余验收

- 用户要求尝试落实四项检查,按已说明方案推进 F12/D3;随后明确批准 Serena 隔离安装与验收,上限新增磁盘 1 GB、安装 15 分钟,不改全局 PATH。
- main 保护已通过 GitHub API 写入并回读:PR 必需、strict Node 22/24 与 CodeQL csharp/javascript-typescript/actions 五项检查,绑定 GitHub Actions app=15368;enforce_admins=true、禁止 force push/删除、要求解决讨论。单维护者 required_approving_review_count=0,不将其写作独立审核。请求/回执在 test-tmp/security-verification/main-protection-*.json;本轮未新增子代理。
- Repomix 健康与打包改为 process.execPath+独立 argv+shell:false。读取本地 package.bin,显式 customCliPath 仅接受绝对 JS 入口;不再调用 npx 缓存/PATH shell 包装器,不安装 Repomix。保留禁用无进程、降级、缓存策略隔离;随机输出名避免并发碰撞,清理超时文件和取消监听器,持续排空并限制输出。
- 初次新增测试错误使用 CacheManager 构造参数/dispose,修正为现有 API 后 15/15 通过;这些是真实 Node 子进程夹具,不是真实 Repomix 包验收。覆盖特殊字符 argv、安装 bin 发现、无效显式路径、超时与取消后 PID 退出。失败证据保留 repomix-targeted.log。
- 完整检查第一轮暴露两项旧测试依赖 npx 恰好较慢;改为真实挂起脚本,不削弱超时/清理断言。第二轮因隔离 Serena 安装扩大工作区,旧全仓影响分析触发真实 total-byte-limit 并正确返回 UNKNOWN;将该测试改为固定两文件夹具,保留有引用风险评估及置信度约束,未放宽生产扫描上限。第三轮完整 check 通过,报告/日志见 test-tmp/security-verification/full-check-3.log。
- Serena 固定 v1.7.0 commit=949a27ef1e5fda1a6e7b561e777bcece345c6ffd,复用现有 Python 3.13.7,venv/uv-cache/SERENA_HOME/语言服务位于 test-tmp/serena-real;安装完成约 632 MB,.NET 10 复用已有环境。C# Roslyn=5.5.0-2.26078.4,下载校验采用上游固定 SHA-256。握手真实成功,第一次查询发现 structuredContent.result 包裹 JSON 字符串被当成符号对象;上游 content 内实际返回三个完整身份。此新缺陷作为后续兼容修复,不把本 PR 写成 Serena 已验收。
- 当前 Codex 管理 CLI 只有 list/get/add/remove/login/logout,没有受支持的 reconnect 命令。保存配置指向正确 I:/WinCode/dist/index.js;没有用重复注册或杀进程替代重连,也未宣称旧父连接更新。
- 作者反证自审:无 shell 不等于已安装脚本可信或有沙盒;缺失显式 CLI 不应悄悄执行另一安装。CodeQL 任务成功仍需合并后读取 alert #1 的 fixed 状态;PR/远端验收待后续回执。
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wincode-mcp",
"version": "0.12.3",
"version": "0.12.4",
"description": "Windows-first MCP gateway: .NET project graph, evidence-bounded context, honest change-impact, long-running process hygiene",
"main": "dist/index.js",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion skills/wincode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: 使用 WinCode MCP 分析 Windows/.NET 工作区,或读取桌面

# WinCode

仓库手册版本:0.12.3。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行,不在每个任务中例行检查。以当前连接实际 Schema 为准,手册版本不证明 MCP 已重连。
仓库手册版本:0.12.4。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行,不在每个任务中例行检查。以当前连接实际 Schema 为准,手册版本不证明 MCP 已重连。

仅按当前任务读取对应手册,不预读全部文件:
- 代码、上下文、引用、影响分析:[code](references/code.md)。
Expand Down
2 changes: 2 additions & 0 deletions skills/wincode/references/diagnostics.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 诊断与审计

从 0.12.4 起 Repomix 健康探测和打包都由当前 Node 可执行文件直接启动已安装的 JavaScript CLI;不经过 cmd、npx 或 PATH 包装脚本,也不下载包。默认按目标工作区和 WinCode 安装目录的 Node 模块路径读取 repomix/package.json 的 bin 入口;不搜索 npx 缓存或 npm 自定义全局前缀。非标准安装需在宿主 WinCodeConfig.adapters.repomix.customCliPath 提供绝对 .js/.cjs/.mjs 路径;该字段不是 MCP 工具参数,不能传给 hello/prepare_context。显式路径无效时返回 builtin fallback,不执行另一份安装;useCli=false 仍完全禁止探测和启动。执行已安装脚本不提供沙盒或脚本可信性保证。

`hello` 从 0.12.1 起只读取版本、能力和已知状态,不启动上游、CLI 或 UI Host 探测进程。`health.healthObservation` 区分 `known/unknown` 并给出 `observedAt`;`unknown`、`available:null` 或 `commandFound:null` 表示尚未探测,不能解释为不可用。配置禁用属于已知策略,但观察时间可为 null。已知健康结果可能陈旧,需要当前检查时调用现有 `wincode_diagnose_project({})`,不向 hello 添加未声明的 force/probe 字段。

代码查询、引用、上下文、影响分析和重构建议接收 MCP 客户端取消信号;停止后续扫描/打包,等待当前读操作或自有上游进程清理后释放请求占用。上游 RPC 取消可能重置共享 Serena 连接,其他上游调用可能失败或降级;不保证外部服务器的单请求取消实现。磁盘单次 OS I/O 不能保证瞬时中断。工作区切换在等待和提交前可取消;已开始提交切换时完成一致性收尾,不声称已回滚。
Expand Down
71 changes: 61 additions & 10 deletions src/Adapters/RepomixAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { spawn, ChildProcess } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto';
import { createRequire } from 'node:module';
import { IAdapter, AdapterHealth, AdapterLastError } from './IAdapter.js';
import { WinCodeConfig, getDefaultTimeouts } from '../Core/Config.js';
import { CacheManager } from '../Core/Cache.js';
Expand All @@ -20,6 +21,7 @@ export class RepomixAdapter implements IAdapter {
private cache: CacheManager;
private resources?: ResourceManager;
private isCliAvailable = false;
private cliEntry: string | null = null;
private activeProcesses: Set<ChildProcess> = new Set();
private healthCache: { at: number; value: AdapterHealth } | null = null;
private inflightPacks = new Map<string, Promise<RepomixPackResult>>();
Expand Down Expand Up @@ -52,6 +54,7 @@ export class RepomixAdapter implements IAdapter {
// Configuration is authoritative even when an earlier probe found an installed CLI.
if (!this.config.adapters.repomix.useCli) {
this.isCliAvailable = false;
this.cliEntry = null;
this.healthCache = null;
return {
available: true,
Expand All @@ -66,11 +69,22 @@ export class RepomixAdapter implements IAdapter {
return this.healthCache.value;
}

this.cliEntry = await this.resolveCliEntry();
if (!this.config.adapters.repomix.useCli) return this.checkHealth(timeoutMs);
if (!this.cliEntry) {
this.isCliAvailable = false;
const health: AdapterHealth = { available: true, source: 'fallback',
details: 'Repomix JavaScript CLI entry not found; using builtin packer. Install locally or configure an absolute customCliPath (.js/.cjs/.mjs); npx caches and shell wrappers are not executed.' };
this.healthCache = { at: Date.now(), value: health };
return health;
}
const health = await new Promise<AdapterHealth>((resolve) => {
let isSettled = false;
const proc = spawn('cmd', ['/c', 'npx --no-install repomix --version'], {
const proc = spawn(process.execPath, [this.cliEntry!, '--version'], {
cwd: this.config.workspaceRoot,
windowsHide: true,
shell: false,
stdio: ['ignore', 'pipe', 'ignore'],
});

this.trackProcess(proc);
Expand All @@ -95,7 +109,7 @@ export class RepomixAdapter implements IAdapter {
}, waitMs);

let stdout = '';
proc.stdout?.on('data', (d) => (stdout += d.toString()));
proc.stdout?.on('data', (d) => { stdout = (stdout + d.toString()).slice(0, 4096); });

proc.on('close', (code) => {
if (isSettled) return;
Expand Down Expand Up @@ -140,9 +154,43 @@ export class RepomixAdapter implements IAdapter {
});

this.healthCache = { at: Date.now(), value: health };
this.isCliAvailable = health.source === 'installed';
return health;
}

private async resolveCliEntry(): Promise<string | null> {
const validEntry = async (entry: string): Promise<string | null> => {
if (!path.isAbsolute(entry) || !/\.(?:cjs|mjs|js)$/i.test(entry)) return null;
return await fs.stat(entry).then(stat => stat.isFile() ? entry : null).catch(() => null);
};
const custom = this.config.adapters.repomix.customCliPath;
// An invalid explicit override must not silently launch a different installation.
if (custom !== undefined) return validEntry(custom);
const searchPaths = new Set([
...(createRequire(path.join(this.config.workspaceRoot, 'package.json')).resolve.paths('repomix') ?? []),
...(createRequire(import.meta.url).resolve.paths('repomix') ?? []),
]);
for (const base of searchPaths) {
const packageRoot = path.join(base, 'repomix');
try {
const manifestPath = path.join(packageRoot, 'package.json');
if ((await fs.stat(manifestPath)).size > 64 * 1024) continue;
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.repomix;
if (manifest.name !== 'repomix' || typeof bin !== 'string' || path.isAbsolute(bin)) continue;
const entry = path.resolve(packageRoot, bin);
const relative = path.relative(packageRoot, entry);
if (relative.startsWith('..') || path.isAbsolute(relative)) continue;
const realRoot = await fs.realpath(packageRoot);
const realEntry = await fs.realpath(entry);
const realRelative = path.relative(realRoot, realEntry);
if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) continue;
if (await validEntry(realEntry)) return realEntry;
} catch { /* Missing or malformed packages leave the builtin packer available. */ }
}
return null;
}

private trackProcess(proc: ChildProcess): void {
this.activeProcesses.add(proc);
this.resources?.registerProcess('repomix', proc);
Expand All @@ -160,7 +208,7 @@ export class RepomixAdapter implements IAdapter {
// A disabled request must neither read a CLI snapshot nor join an enabled CLI pack.
const allowCli = this.config.adapters.repomix.useCli;
const policy = allowCli ? 'cli-enabled' : 'builtin-only';
const cacheKey = `repomix_pack_v3_${policy}_${JSON.stringify(options || {})}_${this.config.workspaceRoot}`;
const cacheKey = `repomix_pack_v4_${policy}_${this.config.adapters.repomix.customCliPath ?? ''}_${JSON.stringify(options || {})}_${this.config.workspaceRoot}`;
const fingerprint = await this.cache.computeWorkspaceFingerprint(this.config.workspaceRoot);

const cached = await this.cache.get<RepomixPackResult>(cacheKey, fingerprint);
Expand Down Expand Up @@ -228,13 +276,12 @@ export class RepomixAdapter implements IAdapter {
if (!this.config.adapters.repomix.useCli) return this.packWithFallback(options, operation);

const ext = style === 'xml' ? 'xml' : 'md';
const tempOutputFile = path.join(tempOutputDir, `repomix_${Date.now()}.${ext}`);
const tempOutputFile = path.join(tempOutputDir, `repomix_${crypto.randomUUID()}.${ext}`);
const entry = this.cliEntry;
if (!entry) throw new Error('Repomix CLI entry has not passed its health check');

const args = [
'/c',
'npx',
'--no-install',
'repomix',
entry,
'--style',
style,
'-o',
Expand All @@ -253,15 +300,17 @@ export class RepomixAdapter implements IAdapter {

return new Promise((resolve, reject) => {
let isSettled = false;
const proc = spawn('cmd', args, {
const proc = spawn(process.execPath, args, {
cwd: root,
windowsHide: true,
shell: false,
stdio: ['ignore', 'ignore', 'pipe'],
});

this.trackProcess(proc);

let stderr = '';
proc.stderr?.on('data', (d) => (stderr += d.toString()));
proc.stderr?.on('data', (d) => { stderr = (stderr + d.toString()).slice(-4096); });

const packTimeoutMs = this.config.timeouts?.repomixPackMs ?? 30_000;
const cancel = async () => {
Expand All @@ -279,8 +328,10 @@ export class RepomixAdapter implements IAdapter {
const timeout = setTimeout(async () => {
if (isSettled) return;
isSettled = true;
operation?.signal?.removeEventListener('abort', cancel);
this.untrackProcess(proc);
await killProcessTree(proc).catch(() => {});
await fs.unlink(tempOutputFile).catch(() => {});
reject(new TimeoutError('repomix', packTimeoutMs));
}, packTimeoutMs);
operation?.signal?.addEventListener('abort', cancel, { once: true });
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path';

export const WINCODE_VERSION = '0.12.3';
export const WINCODE_VERSION = '0.12.4';

/**
* Bounded waits for every external process/RPC. None of these may be Infinity.
Expand Down Expand Up @@ -41,6 +41,7 @@ export interface WinCodeConfig {
adapters: {
repomix: {
useCli: boolean;
/** Absolute installed JavaScript CLI entry (.js/.cjs/.mjs), never a shell wrapper. */
customCliPath?: string;
};
serena: {
Expand Down
Loading