Merge pull request #292 from morluto/fix/npm-entry-points

fix(npm): use latest entry points, remove install scripts, and rename skill
This commit is contained in:
morluto 2026-07-17 09:03:25 +08:00 committed by GitHub
commit bf85ee62c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 573 additions and 230 deletions

View File

@ -105,7 +105,6 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run rebuild:native
- run: npm run test:coverage -- --reporter=verbose --reporter=junit --outputFile.junit=test-results.xml
- uses: actions/upload-artifact@v4
if: always()
@ -128,7 +127,6 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run rebuild:native
- run: npm run verify:package
windows-curated:

View File

@ -48,13 +48,12 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run rebuild:native
- name: Install packed CLI in isolation
shell: bash
run: |
tarball="$(npm pack --silent)"
prefix="${RUNNER_TEMP}/rea-prefix"
npm install --global --prefix "${prefix}" "./${tarball}"
npm install --global --ignore-scripts --prefix "${prefix}" "./${tarball}"
echo "REA_VERIFY_SERVER_COMMAND=${prefix}/bin/rea" >> "${GITHUB_ENV}"
echo 'REA_VERIFY_SERVER_ARGS_JSON=["mcp"]' >> "${GITHUB_ENV}"
- name: Verify two binaries through an alternate launcher symlink

View File

@ -27,13 +27,12 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run rebuild:native
- name: Install packed CLI in isolation
shell: bash
run: |
tarball="$(npm pack --silent)"
prefix="${RUNNER_TEMP}/rea-prefix"
npm install --global --prefix "${prefix}" "./${tarball}"
npm install --global --ignore-scripts --prefix "${prefix}" "./${tarball}"
echo "REA_VERIFY_SERVER_COMMAND=${prefix}/bin/rea" >> "${GITHUB_ENV}"
echo 'REA_VERIFY_SERVER_ARGS_JSON=["mcp"]' >> "${GITHUB_ENV}"
- name: Verify two real binaries

View File

@ -8,7 +8,6 @@ REA development requires Node.js 24.18.x and npm 11.16.x. Real-Hopper verificati
```bash
npm ci
npm run rebuild:native # only when the packaged PTY binary is incompatible
npm test
```
@ -78,8 +77,8 @@ npm publish --access public
After npm registry propagation, verify the published CLI and connect the client SDK version pinned in `package.json` to the published server to confirm the canonical tool catalog:
```bash
npx -y rea-agents --help
npx -y rea-agents doctor
npx -y rea-agents setup --yes
npx -y rea-agents mcp
npx -y rea-agents@latest --help
npx -y rea-agents@latest doctor
npx -y rea-agents@latest setup --yes
npx -y rea-agents@latest mcp
```

View File

@ -117,9 +117,9 @@ Review the setup plan, approve it if appropriate, then describe the app or featu
### From Terminal — no installation
```bash
npx -y rea-agents setup
npx -y rea-agents doctor
npx -y rea-agents analyze /Applications/Notes.app
npx -y rea-agents@latest setup
npx -y rea-agents@latest doctor
npx -y rea-agents@latest analyze /Applications/Notes.app
```
Review the setup plan before confirming it. Restart a configured agent so it loads REA.
@ -146,6 +146,11 @@ an unrelated global package.
Choose either the no-install commands or the global installation. You do not need both.
`npm install rea-agents` without `--global` installs `rea` only into the
current project's `node_modules/.bin`; it does not add `rea` to your shell
`PATH`. Use the `npx` commands above for one-off runs or `--global` when you
want a shell-visible `rea` command.
### Requirements
- macOS 12 or newer
@ -159,7 +164,7 @@ Deep binary operations use [Hopper](https://www.hopperapp.com/), a separate desk
If something is not working, run:
```bash
npx -y rea-agents doctor
npx -y rea-agents@latest doctor
```
`rea doctor --json` is read-only and distinguishes unsupported hosts, missing dependencies, a missing local analysis engine, configuration drift, and healthy checks. Paid-license activation is optional: on Linux, REA runs the supported Hopper demo build on a private Xvfb display and selects Hopper's offered demo mode for each analysis session.
@ -432,7 +437,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents", "mcp"]
"args": ["-y", "rea-agents@latest", "mcp"]
}
}
}
@ -473,20 +478,20 @@ The CLI and MCP server use the same application workflows and evidence contracts
The agent workflow above is the easiest way to use REA. For a one-off overview from the Terminal:
```bash
npx -y rea-agents analyze /Applications/Notes.app
npx -y rea-agents inspect /Applications/Notes.app
npx -y rea-agents inspect /Applications/Notes.app --detail detailed --limit 20
npx -y rea-agents search /Applications/Notes.app "offline"
npx -y rea-agents function /Applications/Notes.app 0x1000
npx -y rea-agents xrefs /Applications/Notes.app 0x1000
npx -y rea-agents trace /Applications/Notes.app "offline"
npx -y rea-agents compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
npx -y rea-agents investigate-versions /path/to/v1 /path/to/v2 /absolute/path/to/evidence/releases.json --yes
npx -y rea-agents capabilities
npx -y rea-agents providers
npx -y rea-agents@latest analyze /Applications/Notes.app
npx -y rea-agents@latest inspect /Applications/Notes.app
npx -y rea-agents@latest inspect /Applications/Notes.app --detail detailed --limit 20
npx -y rea-agents@latest search /Applications/Notes.app "offline"
npx -y rea-agents@latest function /Applications/Notes.app 0x1000
npx -y rea-agents@latest xrefs /Applications/Notes.app 0x1000
npx -y rea-agents@latest trace /Applications/Notes.app "offline"
npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
npx -y rea-agents@latest investigate-versions /path/to/v1 /path/to/v2 /absolute/path/to/evidence/releases.json --yes
npx -y rea-agents@latest capabilities
npx -y rea-agents@latest providers
```
Run `npx -y rea-agents --help` for direct decompilation, bounded search and
Run `npx -y rea-agents@latest --help` for direct decompilation, bounded search and
other options. `analyze` and `inspect` share the same overview workflow;
`function`, `xrefs`, and `trace` return the same Evidence v2 envelopes as MCP.
@ -588,9 +593,9 @@ first terminal, interaction, exit, filesystem, protocol, process, or shim
divergence. See [Process Capture v4](docs/process-capture.md) for scenario
fields, command-shim replay, checkpoint triggers, limits, and safety behavior.
If the native PTY backend is unavailable, install Xcode command-line tools and
run `npm run rebuild:native`. Linux source builds require Python, `make`, and a
C++ toolchain. Compatible packaged binaries do not require this rebuild.
REA installs a prebuilt PTY backend for supported macOS, Linux, and Windows
architectures. If the capability check reports that the backend is unavailable,
reinstall REA for the current platform and architecture.
ASAR inventory verifies Electron integrity metadata for both archive entries
and `.asar.unpacked` companion files. Integrity failures identify the logical

View File

@ -90,7 +90,7 @@ npx skills add morluto/rea
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx -y rea-agents setup
npx -y rea-agents@latest setup
```
إذا طلب macOS أو برنامج التثبيت تأكيدًا، فأكمل الخطوة ثم شغّل الأمر نفسه مرة أخرى.
@ -111,7 +111,7 @@ npx -y rea-agents setup
إذا واجهت مشكلة، فشغّل:
```bash
npx -y rea-agents doctor
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
```
@ -180,7 +180,7 @@ rea uninstall --purge-data
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents", "mcp"]
"args": ["-y", "rea-agents@latest", "mcp"]
}
}
}
@ -206,11 +206,11 @@ flowchart LR
سير عمل الوكيل أعلاه هو أسهل طريقة لاستخدام REA. للحصول على نظرة سريعة على تطبيق من الطرفية:
```bash
npx -y rea-agents analyze /Applications/Notes.app
npx -y rea-agents compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
npx -y rea-agents@latest analyze /Applications/Notes.app
npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
```
شغّل `npx -y rea-agents --help` لمعرفة خيارات فك الترجمة المباشر والخيارات الأخرى.
شغّل `npx -y rea-agents@latest --help` لمعرفة خيارات فك الترجمة المباشر والخيارات الأخرى.
يمكن لـ REA فتح مجلد `.app` على Mac مباشرة. إذا لم يعثر الوكيل على التطبيق، فأخبره بمكان تثبيته.

View File

@ -91,7 +91,7 @@ Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux では、REA が公式の D
```bash
# 1. REA をセットアップ
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx -y rea-agents setup
npx -y rea-agents@latest setup
```
macOS やインストーラーから確認を求められた場合は、その操作を完了してから同じコマンドをもう一度実行してください。
@ -112,7 +112,7 @@ REA でメモアプリをリバースエンジニアリングしてください
問題がある場合は次を実行します:
```bash
npx -y rea-agents doctor
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
```
@ -170,7 +170,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents", "mcp"]
"args": ["-y", "rea-agents@latest", "mcp"]
}
}
}
@ -195,10 +195,10 @@ CLI と MCP サーバーは同じ解析エンジンを使います。ターミ
上のエージェントワークフローが、REA を使う最も簡単な方法です。ターミナルから一度だけアプリの概要を調べる場合は、次を実行します。
```bash
npx -y rea-agents analyze /Applications/Notes.app
npx -y rea-agents@latest analyze /Applications/Notes.app
```
直接デコンパイルする方法やその他のオプションは、`npx -y rea-agents --help` で確認できます。
直接デコンパイルする方法やその他のオプションは、`npx -y rea-agents@latest --help` で確認できます。
グローバルな `rea` コマンドとしてもインストールできます。

View File

@ -91,7 +91,7 @@ Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux에서 REA는 공식 DEB, RPM 또
```bash
# 1. REA 설정
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx -y rea-agents setup
npx -y rea-agents@latest setup
```
macOS나 설치 프로그램이 확인을 요청하면 해당 절차를 완료하고 같은 명령을 다시 실행하세요.
@ -112,7 +112,7 @@ SQLite를 사용해 제 프로젝트에 비슷한 기능을 구현해 주세요.
문제가 있으면 다음을 실행하세요.
```bash
npx -y rea-agents doctor
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
```
@ -170,7 +170,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents", "mcp"]
"args": ["-y", "rea-agents@latest", "mcp"]
}
}
}
@ -195,10 +195,10 @@ CLI와 MCP 서버는 같은 분석 엔진을 사용합니다. 터미널 명령
위의 에이전트 워크플로가 REA를 사용하는 가장 쉬운 방법입니다. 터미널에서 앱을 한 번 빠르게 살펴보려면 다음을 실행하세요.
```bash
npx -y rea-agents analyze /Applications/Notes.app
npx -y rea-agents@latest analyze /Applications/Notes.app
```
직접 디컴파일하는 방법과 기타 옵션은 `npx -y rea-agents --help`에서 확인할 수 있습니다.
직접 디컴파일하는 방법과 기타 옵션은 `npx -y rea-agents@latest --help`에서 확인할 수 있습니다.
전역 `rea` 명령으로 설치할 수도 있습니다.

View File

@ -91,7 +91,7 @@ npx skills add morluto/rea
```bash
# 1. 安装和配置 REA
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx -y rea-agents setup
npx -y rea-agents@latest setup
```
如果 macOS 或安装程序要求确认,请完成提示,然后再次运行同一命令。
@ -112,7 +112,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
如果遇到问题,请运行:
```bash
npx -y rea-agents doctor
npx -y rea-agents@latest doctor
rea uninstall
rea uninstall --purge-data
```
@ -170,7 +170,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents", "mcp"]
"args": ["-y", "rea-agents@latest", "mcp"]
}
}
}
@ -195,10 +195,10 @@ CLI 与 MCP 服务器使用相同的分析引擎。终端命令完成后会关
上面的智能体工作流是使用 REA 最简单的方式。如果只想在终端中快速了解一个应用:
```bash
npx -y rea-agents analyze /Applications/Notes.app
npx -y rea-agents@latest analyze /Applications/Notes.app
```
运行 `npx -y rea-agents --help` 查看直接反编译和其他选项。
运行 `npx -y rea-agents@latest --help` 查看直接反编译和其他选项。
也可以全局安装 `rea` 命令:

View File

@ -20,7 +20,7 @@
</div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol">&lt;</span><a href="../types/application_ProcessCaptureCapability.ProcessCaptureCapability.html" class="tsd-signature-type tsd-kind-type-alias">ProcessCaptureCapability</a><span class="tsd-signature-symbol">&gt;</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/ProcessCaptureCapability.ts#L14">src/application/ProcessCaptureCapability.ts:14</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/ProcessCaptureCapability.ts#L13">src/application/ProcessCaptureCapability.ts:13</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -26,7 +26,7 @@
<li><span><code class="tsd-tag">Optional</code><span class="tsd-kind-parameter">signal</span>: <span class="tsd-signature-type">AbortSignal</span></span></li></ul></div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol">&lt;</span><a href="../types/domain_result.Result.html" class="tsd-signature-type tsd-kind-type-alias">Result</a><span class="tsd-signature-symbol">&lt;</span><a href="../interfaces/domain_processCapture.ProcessCapture.html" class="tsd-signature-type tsd-kind-interface">ProcessCapture</a><span class="tsd-signature-symbol">,</span> <a href="../classes/application_ProcessCaptureError.ProcessCaptureError.html" class="tsd-signature-type tsd-kind-class">ProcessCaptureError</a><span class="tsd-signature-symbol">&gt;</span><span class="tsd-signature-symbol">&gt;</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/ProcessHarness.ts#L494">src/application/ProcessHarness.ts:494</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/ProcessHarness.ts#L495">src/application/ProcessHarness.ts:495</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -28,7 +28,7 @@
<li><span><span class="tsd-kind-parameter">providerEnvironment</span>: <a href="../types/application_Setup.SetupProviderEnvironment.html" class="tsd-signature-type tsd-kind-type-alias">SetupProviderEnvironment</a></span></li></ul></div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-keyword">readonly</span> <a href="../interfaces/application_Setup.SetupAction.html" class="tsd-signature-type tsd-kind-interface">SetupAction</a><span class="tsd-signature-symbol">[]</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupPlan.ts#L8">src/application/SetupPlan.ts:8</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupPlan.ts#L9">src/application/SetupPlan.ts:9</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -24,7 +24,7 @@
<li><span><span class="tsd-kind-parameter">home</span>: <span class="tsd-signature-type">string</span></span></li></ul></div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">boolean</span><span class="tsd-signature-symbol">&gt;</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L26">src/application/SetupSkill.ts:26</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L34">src/application/SetupSkill.ts:34</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -24,7 +24,7 @@
<li><span><span class="tsd-kind-parameter">home</span>: <span class="tsd-signature-type">string</span></span></li></ul></div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">&quot;failed&quot;</span> <span class="tsd-signature-symbol">|</span> <span class="tsd-signature-type">&quot;unchanged&quot;</span> <span class="tsd-signature-symbol">|</span> <span class="tsd-signature-type">&quot;installed&quot;</span><span class="tsd-signature-symbol">&gt;</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L38">src/application/SetupSkill.ts:38</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L50">src/application/SetupSkill.ts:50</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -13,7 +13,7 @@
<h1>Type Alias ProcessCaptureCapability</h1></div>
<div class="tsd-signature"><span class="tsd-kind-type-alias">ProcessCaptureCapability</span><span class="tsd-signature-symbol">:</span><br/>    <span class="tsd-signature-symbol">|</span> <span class="tsd-signature-symbol">{</span> <span class="tsd-kind-property">available</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">true</span><span class="tsd-signature-symbol">;</span> <span class="tsd-kind-property">backend</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">&quot;node-pty&quot;</span> <span class="tsd-signature-symbol">}</span><br/>    <span class="tsd-signature-symbol">|</span> <span class="tsd-signature-symbol">{</span> <span class="tsd-kind-property">available</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">false</span><span class="tsd-signature-symbol">;</span> <span class="tsd-kind-property">backend</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">&quot;node-pty&quot;</span><span class="tsd-signature-symbol">;</span> <span class="tsd-kind-property">reason</span><span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span> <span class="tsd-signature-symbol">}</span></div><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/ProcessCaptureCapability.ts#L4">src/application/ProcessCaptureCapability.ts:4</a></li></ul></aside></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/ProcessCaptureCapability.ts#L3">src/application/ProcessCaptureCapability.ts:3</a></li></ul></aside></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -13,6 +13,11 @@ rea setup
REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
Running `npm install rea-agents` without `--global` installs the executable only
in the current project's `node_modules/.bin`; it does not make `rea` available
on the shell `PATH`. Use `npx -y rea-agents@latest` for one-off commands or the
global installation above for a shell-visible `rea` command.
The optional curl wrapper installs only the global npm package:
```bash

View File

@ -8,7 +8,7 @@
"client": "2.0.0-beta.4",
"core": "2.0.0-beta.4"
},
"skill_version": "20"
"skill_version": "21"
},
"tools": {
"total": 95,

111
package-lock.json generated
View File

@ -7,13 +7,13 @@
"": {
"name": "rea-agents",
"version": "2.0.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
"@babel/types": "8.0.4",
"@electron/asar": "4.2.0",
"@jridgewell/trace-mapping": "0.3.31",
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/server": "2.0.0-beta.4",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
@ -25,7 +25,6 @@
"ignore": "7.0.6",
"incur": "0.4.13",
"isomorphic-git": "1.38.7",
"node-pty": "1.1.0",
"pino": "9.14.0",
"plist": "5.0.0",
"semver": "7.8.5",
@ -245,6 +244,98 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lydell/node-pty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz",
"integrity": "sha512-VDD8LtlMTOrPKWMXUAcB9+LTktzuunqrMwkYR1DMRBkS6LQrCt+0/Ws1o2rMml/n3guePpS7cxhHF7Nm5K4iMw==",
"license": "MIT",
"optionalDependencies": {
"@lydell/node-pty-darwin-arm64": "1.1.0",
"@lydell/node-pty-darwin-x64": "1.1.0",
"@lydell/node-pty-linux-arm64": "1.1.0",
"@lydell/node-pty-linux-x64": "1.1.0",
"@lydell/node-pty-win32-arm64": "1.1.0",
"@lydell/node-pty-win32-x64": "1.1.0"
}
},
"node_modules/@lydell/node-pty-darwin-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.1.0.tgz",
"integrity": "sha512-7kFD+owAA61qmhJCtoMbqj3Uvff3YHDiU+4on5F2vQdcMI3MuwGi7dM6MkFG/yuzpw8LF2xULpL71tOPUfxs0w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@lydell/node-pty-darwin-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.1.0.tgz",
"integrity": "sha512-XZdvqj5FjAMjH8bdp0YfaZjur5DrCIDD1VYiE9EkkYVMDQqRUPHYV3U8BVEQVT9hYfjmpr7dNaELF2KyISWSNA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@lydell/node-pty-linux-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.1.0.tgz",
"integrity": "sha512-yyDBmalCfHpLiQMT2zyLcqL2Fay4Xy7rIs8GH4dqKLnEviMvPGOK7LADVkKAsbsyXBSISL3Lt1m1MtxhPH6ckg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@lydell/node-pty-linux-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.1.0.tgz",
"integrity": "sha512-NcNqRTD14QT+vXcEuqSSvmWY+0+WUBn2uRE8EN0zKtDpIEr9d+YiFj16Uqds6QfcLCHfZmC+Ls7YzwTaqDnanA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@lydell/node-pty-win32-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.1.0.tgz",
"integrity": "sha512-JOMbCou+0fA7d/m97faIIfIU0jOv8sn2OR7tI45u3AmldKoKoLP8zHY6SAvDDnI3fccO1R2HeR1doVjpS7HM0w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@lydell/node-pty-win32-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.1.0.tgz",
"integrity": "sha512-3N56BZ+WDFnUMYRtsrr7Ky2mhWGl9xXcyqR6cexfuCqcz9RNWL+KoXRv/nZylY5dYaXkft4JaR1uVu+roiZDAw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@modelcontextprotocol/client": {
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz",
@ -3792,22 +3883,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-pty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^7.1.0"
}
},
"node_modules/obliterator": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",

View File

@ -42,8 +42,6 @@
"bridge/hopper_bridge.py",
"bridge/ghidra/ReaGhidraBridge.java",
"scripts/rea.mjs",
"scripts/prepare-node-pty.mjs",
"scripts/rebuild-native.mjs",
"scripts/hopper-demo-x11.py",
"skills",
"README.md",
@ -66,7 +64,6 @@
"scan:todos": "node scripts/scan-todos.mjs",
"start": "node scripts/rea.mjs mcp",
"pretest": "npm run build",
"postinstall": "node scripts/prepare-node-pty.mjs",
"test": "vitest run",
"pretest:coverage": "npm run build",
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
@ -96,8 +93,7 @@
"docs:error-schema": "npm run build && node scripts/generate-error-schema.mjs",
"prepack": "npm run deps:check && node scripts/generate-skill-metadata.mjs --check",
"prepublishOnly": "npm run deps:check && node scripts/generate-skill-metadata.mjs --check",
"prepare": "npm run build && husky",
"rebuild:native": "node scripts/rebuild-native.mjs"
"prepare": "npm run build && husky"
},
"lint-staged": {
"*.{ts,mjs,json,md,yml,yaml}": "prettier --write",
@ -108,6 +104,7 @@
"@babel/types": "8.0.4",
"@electron/asar": "4.2.0",
"@jridgewell/trace-mapping": "0.3.31",
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/server": "2.0.0-beta.4",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
@ -119,7 +116,6 @@
"ignore": "7.0.6",
"incur": "0.4.13",
"isomorphic-git": "1.38.7",
"node-pty": "1.1.0",
"pino": "9.14.0",
"plist": "5.0.0",
"semver": "7.8.5",
@ -145,8 +141,5 @@
"typedoc": "0.28.20",
"typescript": "5.9.3",
"vitest": "4.1.10"
},
"allowScripts": {
"node-pty@1.1.0": true
}
}

View File

@ -18,12 +18,12 @@ const lock = JSON.parse(
await readFile(join(root, "package-lock.json"), "utf8"),
);
const skill = await readFile(
join(root, "skills/rea-analysis/SKILL.md"),
join(root, "skills/reverse-engineer-anything/SKILL.md"),
"utf8",
);
const skillVersion = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(skill)?.[1];
if (skillVersion === undefined)
throw new Error("Missing rea-analysis skill metadata version");
throw new Error("Missing reverse-engineer-anything skill metadata version");
const versionOf = (name) => {
const value = lock.packages?.[`node_modules/${name}`]?.version;
if (typeof value !== "string")

View File

@ -10,7 +10,7 @@ for (const argument of arguments_)
throw new Error(`Unknown skill metadata option: ${argument}`);
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const path = join(root, "skills/rea-analysis/SKILL.md");
const path = join(root, "skills/reverse-engineer-anything/SKILL.md");
const current = await readFile(path, "utf8");
const { CATALOG_IDENTITY } = await import(
`${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${String(Date.now())}`

View File

@ -1,35 +0,0 @@
#!/usr/bin/env node
import { chmod, lstat } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
if (process.platform !== "win32") {
const require = createRequire(import.meta.url);
const packageRoot = dirname(
require.resolve("node-pty/package.json", {
paths: [dirname(fileURLToPath(import.meta.url))],
}),
);
const helperPath = join(
packageRoot,
"prebuilds",
`${process.platform}-${process.arch}`,
"spawn-helper",
);
try {
const helper = await lstat(helperPath);
if (!helper.isFile() || helper.isSymbolicLink()) {
throw new Error("node-pty spawn helper is not a regular file");
}
await chmod(helperPath, helper.mode | 0o111);
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
// Source builds place their correctly-modeled helper under build/Release.
} else {
throw error;
}
}
}

View File

@ -35,15 +35,17 @@ const config = {
command: "npx",
args: [
"-y",
JSON.parse(
await readFile(
resolve(
fileURLToPath(new URL("..", import.meta.url)),
"package.json",
`${
JSON.parse(
await readFile(
resolve(
fileURLToPath(new URL("..", import.meta.url)),
"package.json",
),
"utf8",
),
"utf8",
),
).name,
).name
}@latest`,
"mcp",
],
env,

View File

@ -1,22 +0,0 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const child = spawn(npm, ["rebuild", "node-pty"], {
stdio: "inherit",
env: { ...process.env, npm_config_build_from_source: "true" },
});
child.once("error", (error) => {
console.error(`Unable to rebuild node-pty: ${error.message}`);
process.exitCode = 1;
});
child.once("exit", (code, signal) => {
if (signal !== null) {
console.error(`node-pty rebuild terminated by ${signal}`);
process.exitCode = 1;
return;
}
process.exitCode = code ?? 1;
});

View File

@ -11,6 +11,7 @@ import {
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
@ -37,6 +38,16 @@ try {
const packedFiles = (
await exec("tar", ["-tf", join(root, tarball)])
).stdout.split("\n");
const packedManifest = JSON.parse(
(await exec("tar", ["-xOf", join(root, tarball), "package/package.json"]))
.stdout,
);
if (
packedManifest.scripts?.postinstall !== undefined ||
packedManifest.dependencies?.["node-pty"] !== undefined ||
packedManifest.dependencies?.["@lydell/node-pty"] !== "1.1.0"
)
throw new Error("package retained a lifecycle-dependent PTY installation");
if (
packedFiles.some(
(path) => path.includes("__pycache__") || path.endsWith(".pyc"),
@ -121,10 +132,89 @@ try {
};
await exec(
"npm",
["install", "--global", "--prefix", prefix, join(root, tarball)],
[
"install",
"--global",
"--ignore-scripts",
"--prefix",
prefix,
join(root, tarball),
],
{ env: environment },
);
const cli = join(prefix, "bin", "rea");
const processCaptureCapabilityUrl = pathToFileURL(
join(
prefix,
"lib/node_modules/rea-agents/dist/application/ProcessCaptureCapability.js",
),
).href;
const processCaptureCapability = json(
(
await exec(
process.execPath,
[
"--input-type=module",
"--eval",
`const { probeProcessCaptureCapability } = await import(${JSON.stringify(processCaptureCapabilityUrl)}); process.stdout.write(JSON.stringify(await probeProcessCaptureCapability()));`,
],
{ env: environment },
)
).stdout,
);
if (processCaptureCapability.available !== true)
throw new Error(
`packaged PTY backend failed without lifecycle scripts: ${JSON.stringify(processCaptureCapability)}`,
);
const noOptionalPrefix = join(workspace, "prefix-no-optional");
await exec(
"npm",
[
"install",
"--global",
"--ignore-scripts",
"--omit=optional",
"--prefix",
noOptionalPrefix,
join(root, tarball),
],
{ env: environment },
);
const noOptionalCli = join(noOptionalPrefix, "bin", "rea");
await rm(
join(
noOptionalPrefix,
"lib/node_modules/rea-agents/node_modules/@lydell",
`node-pty-${process.platform}-${process.arch}`,
),
{ recursive: true, force: true },
);
const noOptionalCapabilityUrl = pathToFileURL(
join(
noOptionalPrefix,
"lib/node_modules/rea-agents/dist/application/ProcessCaptureCapability.js",
),
).href;
const noOptionalCapability = json(
(
await exec(
process.execPath,
[
"--input-type=module",
"--eval",
`const { probeProcessCaptureCapability } = await import(${JSON.stringify(noOptionalCapabilityUrl)}); process.stdout.write(JSON.stringify(await probeProcessCaptureCapability()));`,
],
{ env: environment },
)
).stdout,
);
if (
!(await run(noOptionalCli, ["--help"], environment)).includes("setup") ||
noOptionalCapability.available !== false
)
throw new Error(
`packaged CLI did not degrade without the optional PTY binary: ${JSON.stringify(noOptionalCapability)}`,
);
const help = await run(cli, ["--help"], environment);
const llms = await run(cli, ["--llms"], environment);
const doctorExecution = await runWithStatus(
@ -662,16 +752,22 @@ try {
);
await run(cli, ["mcp", "add"], environment);
const mcpRegistration = await readFile(npxLog, "utf8");
if (!mcpRegistration.includes("add-mcp npx -y rea-agents mcp --name rea"))
throw new Error("Incur mcp add did not register the floating npx command");
const skillPath = join(home, ".agents/skills/rea-analysis/SKILL.md");
if (
!mcpRegistration.includes("add-mcp npx -y rea-agents@latest mcp --name rea")
)
throw new Error("Incur mcp add did not register the latest npx command");
const skillPath = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
const legacySkillPath = join(home, ".agents/skills/rea-analysis/SKILL.md");
const siblingSkillPath = join(home, ".agents/skills/unrelated/SKILL.md");
if (supportedSetupHost) {
await mkdir(join(home, ".agents/skills/rea-analysis"), {
recursive: true,
});
await mkdir(join(home, ".agents/skills/unrelated"), { recursive: true });
await writeFile(skillPath, "stale managed skill\n");
await writeFile(legacySkillPath, "stale managed skill\n");
await writeFile(siblingSkillPath, "unrelated skill\n");
}
const plannedExecution = await runWithStatus(
@ -684,13 +780,16 @@ try {
const plannedClaudeConfig = await readFile(claudeConfig, "utf8");
const plannedCodexConfig = await readFile(codexTarget, "utf8");
const plannedCursorConfig = await readFile(cursorConfig, "utf8");
const plannedSkill = await readFile(skillPath, "utf8");
const plannedSkill = await readFile(legacySkillPath, "utf8");
if (
planned.status !== "needs_confirmation" ||
plannedExecution.status !== 1 ||
planned.appliedActions.length !== 0 ||
!planned.plannedActions.some(({ kind }) => kind === "configure_client") ||
!planned.plannedActions.some(({ kind }) => kind === "install_skill") ||
!planned.plannedActions.some(
({ kind, detail }) =>
kind === "install_skill" && detail.includes("back up its SKILL.md"),
) ||
plannedClaudeConfig !== '{"existing":true}\n' ||
plannedCodexConfig !== 'model = "gpt-5"\n' ||
plannedCursorConfig !== '{"existing":true}\n' ||
@ -764,17 +863,18 @@ try {
throw new Error("packaged setup did not preserve config symlinks");
const skill = await readFile(skillPath, "utf8");
const canonicalSkill = await readFile(
join(root, "skills/rea-analysis/SKILL.md"),
join(root, "skills/reverse-engineer-anything/SKILL.md"),
"utf8",
);
if (skill !== canonicalSkill)
throw new Error("packaged skill did not match its canonical source");
if (
(await readFile(`${skillPath}.rea.backup`, "utf8")) !==
(await pathExists(legacySkillPath)) ||
(await readFile(`${legacySkillPath}.rea.backup`, "utf8")) !==
"stale managed skill\n" ||
(await readFile(siblingSkillPath, "utf8")) !== "unrelated skill\n"
)
throw new Error("packaged stale-skill upgrade was not isolated");
throw new Error("packaged skill-name migration was not isolated");
const alignedDoctorExecution = await runWithStatus(
cli,
["doctor", "--json"],
@ -1036,7 +1136,7 @@ try {
}
process.stdout.write(
`${JSON.stringify({ cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedRuntimePlanCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: "npx -y rea-agents mcp", doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true })}\n`,
`${JSON.stringify({ cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedRuntimePlanCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: "npx -y rea-agents@latest mcp", lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true })}\n`,
);
} finally {
if (tarball) await rm(join(root, tarball), { force: true });

View File

@ -1,8 +1,8 @@
---
name: rea-analysis
name: reverse-engineer-anything
description: Reverse engineer native, Electron/JavaScript, and web applications with REA. Connect static artifacts to passive runtime evidence, explain how features work, and build a version tailored to the user's project.
metadata:
version: "20"
version: "21"
tool_count: 95
catalog_digest: "b3db06bedadc1ad21a6cfe76f58a9c6cebbf9bce4f9663733b6bab07a57203c3"
---
@ -101,9 +101,9 @@ Notes is only a documentation example. Never select an app unless the user names
## Ensure REA is ready
1. Run `npx -y rea-agents doctor`.
1. Run `npx -y rea-agents@latest doctor`.
2. If setup is needed, tell the user REA needs to install its local binary-analysis tools. Do not lead with implementation details or assume the user knows reverse-engineering products.
3. Before installing external software, obtain approval and identify what will be installed. If deeper analysis needs Hopper, describe it as REA's local analysis engine and note that it is a separate Mac app with its own license. Then run `npx -y rea-agents setup --yes`.
3. Before installing external software, obtain approval and identify what will be installed. If deeper analysis needs Hopper, describe it as REA's local analysis engine and note that it is a separate Mac app with its own license. Then run `npx -y rea-agents@latest setup --yes`.
4. If macOS or an installer requests human input, tell the user exactly what needs attention. After they finish, rerun setup and doctor.
5. If setup registers a new MCP server, tell the user to restart their agent to load all REA tools. Direct CLI commands remain available before restart.

View File

@ -78,7 +78,7 @@ const registrationAligned = (
command.length === 4 &&
command[0] === "npx" &&
command[1] === "-y" &&
command[2] === PRODUCT_IDENTITY.packageName &&
command[2] === PRODUCT_IDENTITY.packageSpecifier &&
command[3] === "mcp"
)
return true;

View File

@ -1,5 +1,4 @@
import { tmpdir } from "node:os";
import { spawn } from "node-pty";
export type ProcessCaptureCapability =
| { readonly available: true; readonly backend: "node-pty" }
@ -13,6 +12,7 @@ export type ProcessCaptureCapability =
export const probeProcessCaptureCapability =
async (): Promise<ProcessCaptureCapability> => {
try {
const { spawn } = await import("@lydell/node-pty");
const terminal = spawn(
process.platform === "win32" ? "cmd.exe" : "/bin/sh",
process.platform === "win32" ? ["/c", "exit", "0"] : ["-c", "exit 0"],

View File

@ -3,7 +3,7 @@ import { createReadStream } from "node:fs";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { IPty } from "node-pty";
import type { IPty } from "@lydell/node-pty";
import type {
FilesystemCheckpoint,

View File

@ -1,4 +1,4 @@
import { spawn, type IPty } from "node-pty";
import type { IPty } from "@lydell/node-pty";
import type {
InteractionEvent,
ProcessCapture,
@ -204,6 +204,7 @@ const startCaptureRuntime = async (options: {
let shimReplay: CommandShimReplay | undefined;
let terminal: IPty | undefined;
try {
const { spawn } = await import("@lydell/node-pty");
replay = await startLoopbackReplay(scenario);
const startedAt = new Date();
const started = Date.now();

View File

@ -53,7 +53,7 @@ export {
const registrationCommand = (): readonly string[] =>
process.env.npm_command === "exec"
? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
? ["npx", "-y", PRODUCT_IDENTITY.packageSpecifier, "mcp"]
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
/** Result of one backup/write/readback transaction. */
@ -405,7 +405,7 @@ export const configureJsonClient = async (
command: readonly string[] = [
"npx",
"-y",
PRODUCT_IDENTITY.packageName,
PRODUCT_IDENTITY.packageSpecifier,
"mcp",
],
): Promise<ClientConfigurationResult> => {
@ -483,7 +483,7 @@ export const configureTomlClient = async (
command: readonly string[] = [
"npx",
"-y",
PRODUCT_IDENTITY.packageName,
PRODUCT_IDENTITY.packageSpecifier,
"mcp",
],
): Promise<ClientConfigurationResult> => {

View File

@ -3,6 +3,7 @@ import type {
SetupClient,
SetupProviderEnvironment,
} from "./Setup.js";
import { PRODUCT_IDENTITY } from "../identity.js";
/** Build the complete setup mutation plan before approval. */
export const setupPlan = (
@ -42,8 +43,9 @@ export const setupPlan = (
? [
{
kind: "install_skill" as const,
target: "~/.agents/skills/rea-analysis/SKILL.md",
detail: "Install or update the bundled REA analysis skill.",
target: `~/.agents/skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`,
detail:
"Install or update the bundled REA reverse-engineering skill. If the retired rea-analysis skill exists, back up its SKILL.md beside the retired path, then remove that legacy entry.",
external: false,
},
]

View File

@ -1,18 +1,26 @@
import { mkdir, readFile, rm } from "node:fs/promises";
import { lstat, mkdir, readFile, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import writeFileAtomic from "write-file-atomic";
import { PRODUCT_IDENTITY } from "../identity.js";
const skillDestination = (home: string, name: string): string =>
join(home, ".agents/skills", name, "SKILL.md");
const readOptionalText = async (path: string): Promise<string | undefined> => {
try {
return await readFile(path, "utf8");
} catch (cause: unknown) {
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
return undefined;
throw cause;
}
};
const canonicalSkillFiles = async (
home: string,
): Promise<{ readonly content: string; readonly destination: string }> => ({
destination: join(
home,
".agents/skills",
PRODUCT_IDENTITY.skillName,
"SKILL.md",
),
destination: skillDestination(home, PRODUCT_IDENTITY.skillName),
content: await readFile(
new URL(
`../../skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`,
@ -28,7 +36,11 @@ export const canonicalSkillNeedsInstall = async (
): Promise<boolean> => {
try {
const { content, destination } = await canonicalSkillFiles(home);
return (await readFile(destination, "utf8")) !== content;
if ((await readOptionalText(destination)) !== content) return true;
for (const name of PRODUCT_IDENTITY.legacySkillNames)
if ((await readOptionalText(skillDestination(home, name))) !== undefined)
return true;
return false;
} catch {
return true;
}
@ -38,45 +50,80 @@ export const canonicalSkillNeedsInstall = async (
export const installCanonicalSkill = async (
home: string,
): Promise<"installed" | "unchanged" | "failed"> => {
let destination = join(
home,
".agents/skills",
PRODUCT_IDENTITY.skillName,
"SKILL.md",
);
let destination = skillDestination(home, PRODUCT_IDENTITY.skillName);
let backup = `${destination}.rea.backup`;
let original: string | undefined;
let canonicalChanged = false;
const removedLegacy: Array<{
readonly content: string;
readonly destination: string;
}> = [];
try {
const canonical = await canonicalSkillFiles(home);
destination = canonical.destination;
backup = `${destination}.rea.backup`;
const { content } = canonical;
original = await readFile(destination, "utf8").catch(() => undefined);
if (original === content) return "unchanged";
original = await readOptionalText(destination);
const legacy: Array<{
readonly content: string;
readonly destination: string;
}> = [];
for (const name of PRODUCT_IDENTITY.legacySkillNames) {
const legacyDestination = skillDestination(home, name);
const legacyContent = await readOptionalText(legacyDestination);
if (legacyContent !== undefined) {
if ((await lstat(dirname(legacyDestination))).isSymbolicLink())
throw new Error("legacy skill directory is a symbolic link");
legacy.push({ content: legacyContent, destination: legacyDestination });
}
}
if (original === content && legacy.length === 0) return "unchanged";
await mkdir(dirname(destination), { recursive: true });
if (original !== undefined)
if (original !== undefined && original !== content)
await writeFileAtomic(backup, original, {
encoding: "utf8",
mode: 0o600,
});
await writeFileAtomic(destination, content, {
encoding: "utf8",
mode: 0o600,
});
canonicalChanged = original !== content;
if (canonicalChanged)
await writeFileAtomic(destination, content, {
encoding: "utf8",
mode: 0o600,
});
if ((await readFile(destination, "utf8")) !== content)
throw new Error("skill readback mismatch");
for (const legacySkill of legacy) {
await writeFileAtomic(
`${legacySkill.destination}.rea.backup`,
legacySkill.content,
{ encoding: "utf8", mode: 0o600 },
);
await rm(legacySkill.destination, { force: true });
removedLegacy.push(legacySkill);
}
return "installed";
} catch {
try {
if (original === undefined) await rm(destination, { force: true });
else
if (canonicalChanged && original === undefined)
await rm(destination, { force: true });
else if (canonicalChanged && original !== undefined)
await writeFileAtomic(destination, original, {
encoding: "utf8",
mode: 0o600,
});
} catch {
// The backup remains beside the skill for explicit operator recovery.
// The canonical backup remains beside the skill for operator recovery.
}
for (const legacySkill of removedLegacy)
try {
await mkdir(dirname(legacySkill.destination), { recursive: true });
await writeFileAtomic(legacySkill.destination, legacySkill.content, {
encoding: "utf8",
mode: 0o600,
});
} catch {
// The legacy backup remains beside the skill for operator recovery.
}
return "failed";
}
};

View File

@ -87,12 +87,7 @@ export const systemUninstallHost = (
): UninstallHost => ({
clients: () => Promise.resolve(supportedClients(home)),
removeClient: (client) => removeClient(client, fileSystem),
removeSkill: () =>
removeManagedPath(
join(home, ".agents/skills", PRODUCT_IDENTITY.skillName),
"skill",
fileSystem,
),
removeSkill: () => removeManagedSkills(home, fileSystem),
purgeData: async () => [
await removeManagedPath(join(home, ".rea/cache"), "cache", fileSystem),
await removeManagedPath(join(home, ".rea/state"), "state", fileSystem),
@ -229,8 +224,10 @@ const isOwnedRegistration = (value: unknown): boolean => {
args.length === 1 &&
args[0] === "mcp") ||
(command === "npx" &&
JSON.stringify(args) ===
JSON.stringify(["-y", PRODUCT_IDENTITY.packageName, "mcp"]))
(JSON.stringify(args) ===
JSON.stringify(["-y", PRODUCT_IDENTITY.packageSpecifier, "mcp"]) ||
JSON.stringify(args) ===
JSON.stringify(["-y", PRODUCT_IDENTITY.packageName, "mcp"])))
);
};
@ -260,6 +257,36 @@ const removeManagedPath = async (
}
};
const removeManagedSkills = async (
home: string,
fileSystem: UninstallFileSystem,
): Promise<UninstallItem> => {
const results: UninstallItem[] = [];
for (const name of [
PRODUCT_IDENTITY.skillName,
...PRODUCT_IDENTITY.legacySkillNames,
])
results.push(
await removeManagedPath(
join(home, ".agents/skills", name),
"skill",
fileSystem,
),
);
const failed = results.find(({ status }) => status === "failed");
if (failed !== undefined) return failed;
const retained = results.find(({ status }) => status === "retained");
if (retained !== undefined) return retained;
const removed = results.filter(({ status }) => status === "removed");
if (removed.length > 0)
return item(
"skill",
"removed",
removed.map(({ detail }) => detail).join(" "),
);
return item("skill", "skipped", "No managed REA skill installation exists.");
};
const item = (
name: string,
status: UninstallItem["status"],

View File

@ -5,5 +5,5 @@ export const PACKAGE_METADATA = {
serverSdkVersion: "2.0.0-beta.4",
clientSdkVersion: "2.0.0-beta.4",
coreSdkVersion: "2.0.0-beta.4",
skillVersion: "20",
skillVersion: "21",
} as const;

View File

@ -6,11 +6,13 @@
export const PRODUCT_IDENTITY = {
displayName: "REA",
packageName: PACKAGE_METADATA.name,
packageSpecifier: `${PACKAGE_METADATA.name}@latest`,
packageVersion: PACKAGE_METADATA.version,
cliBinary: "rea",
mcpCommand: "npx -y rea-agents mcp",
mcpCommand: `npx -y ${PACKAGE_METADATA.name}@latest mcp`,
mcpServerKey: "rea",
skillName: "rea-analysis",
skillName: "reverse-engineer-anything",
legacySkillNames: ["rea-analysis"],
skillVersion: PACKAGE_METADATA.skillVersion,
configFileName: "rea.json",
} as const;

View File

@ -144,7 +144,7 @@ describe("analysis snapshots", () => {
});
it("writes canonical private v2 JSON and rejects changed query identity", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-analysis-snapshot-"));
directory = await mkdtemp(join(tmpdir(), "rea-snapshot-"));
const path = join(directory, "analysis.json");
const target = snapshotTarget(TARGET);
const binding = snapshotBinding(PROFILE);

View File

@ -17,7 +17,7 @@ describe("client registration status", () => {
]);
await writeFile(
join(home, ".codex/config.toml"),
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents", "mcp"]\n',
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@latest", "mcp"]\n',
);
await writeFile(
join(home, ".cursor/mcp.json"),
@ -44,4 +44,23 @@ describe("client registration status", () => {
);
expect(JSON.stringify(statuses)).not.toContain("SECRET");
});
it("reports an unversioned npx registration as stale", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-registrations-"));
await mkdir(join(home, ".codex"));
await writeFile(
join(home, ".codex/config.toml"),
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents", "mcp"]\n',
);
const statuses = await readClientRegistrationStatuses(home);
expect(statuses).toEqual([
expect.objectContaining({
client: "codex",
command: ["npx", "-y", "rea-agents", "mcp"],
state: "stale",
}),
]);
});
});

View File

@ -6,7 +6,7 @@ import { z } from "zod";
import { PRODUCT_IDENTITY } from "../src/identity.js";
describe("package identity", () => {
it("keeps the manifest, executable, and floating MCP command aligned", async () => {
it("keeps the manifest, executable, and latest MCP command aligned", async () => {
const packageJson = z
.object({
name: z.string(),
@ -28,7 +28,12 @@ describe("package identity", () => {
);
expect(packageJson.name).toBe(PRODUCT_IDENTITY.packageName);
expect(Object.keys(packageJson.bin)).toEqual([PRODUCT_IDENTITY.cliBinary]);
expect(PRODUCT_IDENTITY.mcpCommand).toBe(`npx -y ${packageJson.name} mcp`);
expect(PRODUCT_IDENTITY.packageSpecifier).toBe(
`${packageJson.name}@latest`,
);
expect(PRODUCT_IDENTITY.mcpCommand).toBe(
`npx -y ${packageJson.name}@latest mcp`,
);
expect(packageJson.scripts.start).toBe("node scripts/rea.mjs mcp");
expect(packageJson.scripts.prepare).toBe("npm run build && husky");
expect(packageJson.scripts.prepack).toBe(

View File

@ -0,0 +1,30 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
describe("package installation workflows", () => {
it("runs package E2E without the retired native rebuild script", async () => {
const continuousIntegration = await readFile(
new URL("../.github/workflows/ci.yml", import.meta.url),
"utf8",
);
const realHopperLinux = await readFile(
new URL("../.github/workflows/real-hopper-linux.yml", import.meta.url),
"utf8",
);
const realHopperMac = await readFile(
new URL("../.github/workflows/real-hopper.yml", import.meta.url),
"utf8",
);
expect(continuousIntegration).toContain("os: [ubuntu-latest, macos-14]");
expect(continuousIntegration).toContain("npm run verify:package");
for (const workflow of [
continuousIntegration,
realHopperLinux,
realHopperMac,
])
expect(workflow).not.toContain("npm run rebuild:native");
for (const workflow of [realHopperLinux, realHopperMac])
expect(workflow).toContain("npm install --global --ignore-scripts");
});
});

View File

@ -61,10 +61,10 @@ describe("localized README product facts", () => {
expect(content).toContain(
"curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash",
);
expect(content).toContain("npx -y rea-agents setup");
expect(content).toContain("npx -y rea-agents doctor");
expect(content).toContain("npx -y rea-agents@latest setup");
expect(content).toContain("npx -y rea-agents@latest doctor");
expect(content).toContain("rea uninstall");
expect(content).toContain('"args": ["-y", "rea-agents", "mcp"]');
expect(content).toContain('"args": ["-y", "rea-agents@latest", "mcp"]');
expect(content).toContain("Node.js 22");
expect(content).toContain("macOS 12");
expect(content).toContain("Ubuntu 24.04");
@ -81,7 +81,8 @@ describe("localized README product facts", () => {
it("keeps both English CLI onboarding paths discoverable", async () => {
const content = await readFile(resolve("README.md"), "utf8");
expect(content).toContain("npx -y rea-agents analyze");
expect(content).toContain("npx -y rea-agents@latest analyze");
expect(content).toContain("npm install rea-agents` without `--global`");
expect(content).toContain("npm install --global rea-agents");
expect(content).toContain("rea setup");
expect(content).toContain("--install-hopper");

View File

@ -152,6 +152,10 @@ describe("setup workflow", () => {
"configure_client",
"install_skill",
]);
expect(
result.plannedActions.find(({ kind }) => kind === "install_skill")
?.detail,
).toContain("back up its SKILL.md");
expect(host.hopperInstalls).toBe(0);
expect(host.configurations).toBe(0);
expect(result.remediation).toBe(

View File

@ -55,7 +55,7 @@ describe("JSON client configuration transaction", () => {
other: { command: "other" },
rea: {
command: "npx",
args: ["-y", "rea-agents", "mcp"],
args: ["-y", "rea-agents@latest", "mcp"],
},
},
});
@ -79,7 +79,7 @@ describe("JSON client configuration transaction", () => {
mcpServers: {
rea: {
command: "npx",
args: ["-y", "rea-agents", "mcp"],
args: ["-y", "rea-agents@latest", "mcp"],
},
},
});
@ -108,7 +108,7 @@ describe("JSON client configuration transaction", () => {
const configPath = join(directory, "mcp.json");
await writeFile(
configPath,
'{"mcpServers":{"rea":{"command":"npx","args":["-y","rea-agents","mcp"]}}}\n',
'{"mcpServers":{"rea":{"command":"npx","args":["-y","rea-agents@latest","mcp"]}}}\n',
);
expect(await configureJsonClient({ name: "cursor", configPath })).toEqual({
status: "unchanged",
@ -118,6 +118,30 @@ describe("JSON client configuration transaction", () => {
).rejects.toThrow();
});
it("migrates an unversioned npx registration and preserves sibling configuration", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
const configPath = join(directory, "mcp.json");
const original =
'{"theme":"dark","mcpServers":{"rea":{"command":"npx","args":["-y","rea-agents","mcp"]},"other":{"command":"other"}}}\n';
await writeFile(configPath, original);
expect(await configureJsonClient({ name: "cursor", configPath })).toEqual(
expect.objectContaining({ status: "configured" }),
);
expect(JSON.parse(await readFile(configPath, "utf8"))).toEqual({
theme: "dark",
mcpServers: {
rea: {
command: "npx",
args: ["-y", "rea-agents@latest", "mcp"],
},
other: { command: "other" },
},
});
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
});
it("persists a custom Hopper launcher and remains idempotent", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-setup-"));
const configPath = join(directory, "mcp.json");
@ -132,7 +156,7 @@ describe("JSON client configuration transaction", () => {
mcpServers: {
rea: {
command: "npx",
args: ["-y", "rea-agents", "mcp"],
args: ["-y", "rea-agents@latest", "mcp"],
env: { HOPPER_LAUNCHER_PATH: hopperPath },
},
},

View File

@ -146,9 +146,14 @@ describe("agent lifecycle", () => {
const home = await mkdtemp(join(tmpdir(), "rea-uninstall-"));
roots.push(home);
const cursor = join(home, ".cursor/mcp.json");
const skill = join(home, ".agents/skills/rea-analysis/SKILL.md");
const skill = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
const legacySkill = join(home, ".agents/skills/rea-analysis/SKILL.md");
await mkdir(dirname(cursor), { recursive: true });
await mkdir(dirname(skill), { recursive: true });
await mkdir(dirname(legacySkill), { recursive: true });
await writeFile(
cursor,
JSON.stringify({
@ -159,6 +164,7 @@ describe("agent lifecycle", () => {
}),
);
await writeFile(skill, "managed");
await writeFile(legacySkill, "legacy managed");
await mkdir(join(home, ".rea"), { recursive: true });
await import("node:fs/promises").then(({ symlink }) =>
symlink(home, join(home, ".rea/cache")),
@ -168,6 +174,8 @@ describe("agent lifecycle", () => {
expect(JSON.parse(await readFile(cursor, "utf8"))).toEqual({
mcpServers: { other: { command: "other" } },
});
await expect(readFile(skill, "utf8")).rejects.toThrow();
await expect(readFile(legacySkill, "utf8")).rejects.toThrow();
expect(first.items).toContainEqual(
expect.objectContaining({
name: "cache",
@ -300,7 +308,7 @@ describe("agent lifecycle", () => {
it("reports a managed-path removal failure", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-uninstall-remove-"));
roots.push(home);
const skillRoot = join(home, ".agents/skills/rea-analysis");
const skillRoot = join(home, ".agents/skills/reverse-engineer-anything");
await mkdir(skillRoot, { recursive: true });
const result = await runUninstall(
false,

View File

@ -1,4 +1,11 @@
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import {
mkdtemp,
mkdir,
readFile,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
@ -19,7 +26,10 @@ describe("canonical skill transaction", () => {
it("backs up and upgrades a stale managed skill without touching siblings", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
roots.push(home);
const destination = join(home, ".agents/skills/rea-analysis/SKILL.md");
const destination = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
const sibling = join(home, ".agents/skills/unrelated/SKILL.md");
await mkdir(dirname(destination), { recursive: true });
await mkdir(dirname(sibling), { recursive: true });
@ -31,7 +41,7 @@ describe("canonical skill transaction", () => {
expect(await readFile(`${destination}.rea.backup`, "utf8")).toBe(
"stale managed skill\n",
);
expect(await readFile(destination, "utf8")).toContain('version: "20"');
expect(await readFile(destination, "utf8")).toContain('version: "21"');
expect(await readFile(destination, "utf8")).toContain(
`tool_count: ${String(TOOL_CONTRACTS.length)}`,
);
@ -39,4 +49,49 @@ describe("canonical skill transaction", () => {
expect(await canonicalSkillNeedsInstall(home)).toBe(false);
expect(await installCanonicalSkill(home)).toBe("unchanged");
});
it("backs up and retires the legacy skill name", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
roots.push(home);
const legacy = join(home, ".agents/skills/rea-analysis/SKILL.md");
const destination = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
await mkdir(dirname(legacy), { recursive: true });
await writeFile(legacy, "legacy managed skill\n");
expect(await canonicalSkillNeedsInstall(home)).toBe(true);
expect(await installCanonicalSkill(home)).toBe("installed");
expect(await readFile(destination, "utf8")).toContain(
"name: reverse-engineer-anything",
);
expect(await readFile(`${legacy}.rea.backup`, "utf8")).toBe(
"legacy managed skill\n",
);
await expect(readFile(legacy, "utf8")).rejects.toThrow();
expect(await canonicalSkillNeedsInstall(home)).toBe(false);
});
it("does not migrate a legacy skill through a directory symlink", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
roots.push(home);
const skillsRoot = join(home, ".agents/skills");
const external = join(home, "external-skill");
const legacyRoot = join(skillsRoot, "rea-analysis");
const legacy = join(legacyRoot, "SKILL.md");
const destination = join(skillsRoot, "reverse-engineer-anything/SKILL.md");
await mkdir(skillsRoot, { recursive: true });
await mkdir(external, { recursive: true });
await writeFile(join(external, "SKILL.md"), "external skill\n");
await symlink(
external,
legacyRoot,
process.platform === "win32" ? "junction" : "dir",
);
expect(await installCanonicalSkill(home)).toBe("failed");
expect(await readFile(legacy, "utf8")).toBe("external skill\n");
await expect(readFile(destination, "utf8")).rejects.toThrow();
});
});