Compare commits

..

7 Commits

Author SHA1 Message Date
Affaan Mustafa 0197d08fb5 docs: restore hero banner with ECC wordmark, v2.0.0 badge, and brand lettermark
Recreates the v1.10 hero banner design (sourced from commit 602894ef)
that PR #2225 replaced with a plain HTML header:

- Wordmark and breadcrumb now read ECC / affaan-m/ECC
- Version badge reads v2.0.0 · Jun 2026, eyebrow updated to V2.0
- Top-left mark is the actual assets/ecc-icon.svg lettermark (amber E,
  coral CC) instead of a generic coral square
- Catalog columns refreshed with live counts (261 skills, 64 agents,
  84 commands, 409 catalog) and real item names from the repo
- Harness pills updated to the current README list (Claude Code, Codex,
  Cursor, OpenCode, Gemini, Zed, Copilot)
- SVG source committed as assets/hero.svg so future edits never need
  image archaeology; rendered to PNG at 2400x1350 via sharp

README hero line restored to the markdown image; badges, sponsor table,
and guide cards from #2225 kept intact.
2026-06-11 01:15:08 -04:00
Affaan Mustafa 3bdb4a5e12 docs: restore on-brand ECC header, consolidate sponsor placement, make guide links visual (#2225)
- Replace off-brand hero PNG (wrong product name + baked version) with a
  centered HTML header using assets/ecc-icon.svg, h1, and tagline
- Consolidate duplicated sponsor sections: polished centered sponsor table
  at top (CodeRabbit, Greptile, community sponsors, sponsor links); bottom
  section reduced to a one-liner pointing to SPONSORS.md
- Convert guide links to visual cards using the guides' own header images,
  linked to the local guide files
- Fix broken tmux video URL in the shortform guide to the in-repo asset
2026-06-10 23:48:02 -04:00
dependabot[bot] 3aab460b14 chore(deps): bump the cargo-minor-and-patch group (#2207)
Bumps the cargo-minor-and-patch group in /ecc2 with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [ratatui](https://github.com/ratatui/ratatui) | `0.30.0` | `0.30.1` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.50.0` | `1.52.3` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.149` | `1.0.150` |
| [regex](https://github.com/rust-lang/regex) | `1.12.3` | `1.12.4` |
| [clap](https://github.com/clap-rs/clap) | `4.6.0` | `4.6.1` |
| [libc](https://github.com/rust-lang/libc) | `0.2.183` | `0.2.186` |
| [chrono](https://github.com/chronotope/chrono) | `0.4.44` | `0.4.45` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.22.0` | `1.23.3` |


Updates `ratatui` from 0.30.0 to 0.30.1
- [Release notes](https://github.com/ratatui/ratatui/releases)
- [Changelog](https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ratatui/ratatui/compare/ratatui-v0.30.0...ratatui-v0.30.1)

Updates `tokio` from 1.50.0 to 1.52.3
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.50.0...tokio-1.52.3)

Updates `serde_json` from 1.0.149 to 1.0.150
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150)

Updates `regex` from 1.12.3 to 1.12.4
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.12.3...1.12.4)

Updates `clap` from 4.6.0 to 4.6.1
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.0...clap_complete-v4.6.1)

Updates `libc` from 0.2.183 to 0.2.186
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.186/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.183...0.2.186)

Updates `chrono` from 0.4.44 to 0.4.45
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.44...v0.4.45)

Updates `uuid` from 1.22.0 to 1.23.3
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.22.0...v1.23.3)

---
updated-dependencies:
- dependency-name: ratatui
  dependency-version: 0.30.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: tokio
  dependency-version: 1.52.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
- dependency-name: serde_json
  dependency-version: 1.0.150
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: regex
  dependency-version: 1.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: clap
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: libc
  dependency-version: 0.2.186
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: chrono
  dependency-version: 0.4.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: uuid
  dependency-version: 1.23.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 23:39:05 -04:00
dependabot[bot] 7ccc65f550 chore(deps-dev): bump the npm-minor-and-patch group across 1 directory with 2 updates (#2205)
Bumps the npm-minor-and-patch group with 2 updates in the / directory: @opencode-ai/plugin and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@opencode-ai/plugin` from 1.15.3 to 1.16.2

Updates `@types/node` from 25.7.0 to 25.9.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@opencode-ai/plugin"
  dependency-version: 1.16.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
- dependency-name: "@types/node"
  dependency-version: 25.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 23:39:03 -04:00
dependabot[bot] d71ffd56b9 chore(deps): bump actions/setup-node (#2204)
Bumps the actions-minor-and-patch group with 1 update in the / directory: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 6.3.0 to 6.4.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 23:39:00 -04:00
ECC Test c888d2b73f docs: update Greptile sponsor placement 2026-06-09 23:53:13 -04:00
Affaan Mustafa ff768db363 feat(mcp): single-connector default set + connector policy (#2219)
Reduce the default .mcp.json to one connector (chrome-devtools) per the
new policy in docs/MCP-CONNECTOR-POLICY.md: a default earns its slot only
if it is universal AND MCP beats a CLI/API wrapped in a skill. June 2026
audit verdicts: github -> gh via github-ops skill; context7 -> REST via
documentation-lookup; exa -> harness-native search (+ exa-search skill);
memory -> native harness memory + instincts; playwright -> playwright CLI
skills (vendor moved agent flows off MCP); sequential-thinking -> native
extended thinking. All six remain opt-in in mcp-configs/mcp-servers.json.
Tests updated: plugin-manifest policy assertions + install-apply Cursor
expectations.

Co-authored-by: ECC Test <ecc@example.test>
2026-06-09 23:28:35 -04:00
22 changed files with 407 additions and 374 deletions
+3 -6
View File
@@ -49,12 +49,9 @@ stay below provider length limits.
| Server | Purpose | | Server | Purpose |
|---|---| |---|---|
| `github` | GitHub API access | | `chrome-devtools` | Interactive browser debugging via Chrome DevTools (CDP sessions, performance traces, console/network inspection) |
| `context7` | Live documentation lookup |
| `exa` | Neural web search | The former defaults (`github`, `context7`, `exa`, `memory`, `playwright`, `sequential-thinking`) were retired in the June 2026 connector audit — their jobs are covered by skills wrapping CLIs/REST APIs or by harness-native features. They remain available as opt-in entries in `mcp-configs/mcp-servers.json`. See `docs/MCP-CONNECTOR-POLICY.md` for the policy and the per-connector rationale.
| `memory` | Persistent memory across sessions |
| `playwright` | Browser automation & E2E testing |
| `sequential-thinking` | Step-by-step reasoning |
## Notes ## Notes
+5 -5
View File
@@ -40,7 +40,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js ${{ matrix.node }} - name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
@@ -120,7 +120,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
@@ -183,7 +183,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
@@ -210,7 +210,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
@@ -239,7 +239,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
- name: Check for outdated packages - name: Check for outdated packages
@@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
- name: Run security audit - name: Run security audit
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
@@ -131,7 +131,7 @@ jobs:
name: ecc-release-artifacts name: ecc-release-artifacts
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
+2 -2
View File
@@ -48,7 +48,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
@@ -148,7 +148,7 @@ jobs:
name: ecc-release-artifacts name: ecc-release-artifacts
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: ${{ inputs.node-version }} node-version: ${{ inputs.node-version }}
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: ${{ inputs.node-version }} node-version: ${{ inputs.node-version }}
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '20.x' node-version: '20.x'
+2 -22
View File
@@ -1,28 +1,8 @@
{ {
"mcpServers": { "mcpServers": {
"github": { "chrome-devtools": {
"command": "npx", "command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github@2025.4.8"] "args": ["-y", "chrome-devtools-mcp@latest"]
},
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@2.1.4"]
},
"exa": {
"type": "http",
"url": "https://mcp.exa.ai/mcp"
},
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory@2026.1.26"]
},
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp@0.0.69", "--extension"]
},
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking@2025.12.18"]
} }
} }
} }
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## Unreleased
### Changed
- Default MCP connector set reduced to a single connector (`chrome-devtools`) per the new connector policy (`docs/MCP-CONNECTOR-POLICY.md`). The six previous defaults (`github`, `context7`, `exa`, `memory`, `playwright`, `sequential-thinking`) were retired after the June 2026 audit: their jobs are covered by skills wrapping CLIs/REST APIs (`github-ops`, `documentation-lookup`, `exa-search`, e2e skills) or by harness-native features (memory, extended thinking, web search). All six remain opt-in via `mcp-configs/mcp-servers.json`.
## 2.0.0 - 2026-06-09 ## 2.0.0 - 2026-06-09
### Added ### Added
+47 -35
View File
@@ -1,8 +1,6 @@
**Language:** English | [Português (Brasil)](docs/pt-BR/README.md) | [简体中文](README.zh-CN.md) | [繁體中文](docs/zh-TW/README.md) | [日本語](docs/ja-JP/README.md) | [한국어](docs/ko-KR/README.md) | [Türkçe](docs/tr/README.md) | [Русский](docs/ru/README.md) | [Tiếng Việt](docs/vi-VN/README.md) | [ไทย](docs/th/README.md) | [Deutsch](docs/de-DE/README.md) | [Español](docs/es/README.md) **Language:** English | [Português (Brasil)](docs/pt-BR/README.md) | [简体中文](README.zh-CN.md) | [繁體中文](docs/zh-TW/README.md) | [日本語](docs/ja-JP/README.md) | [한국어](docs/ko-KR/README.md) | [Türkçe](docs/tr/README.md) | [Русский](docs/ru/README.md) | [Tiếng Việt](docs/vi-VN/README.md) | [ไทย](docs/th/README.md) | [Deutsch](docs/de-DE/README.md) | [Español](docs/es/README.md)
# ECC ![ECC — the agent harness operating system](assets/hero.png)
![ECC - the harness-native operator system for agentic work](assets/hero.png)
[![Stars](https://img.shields.io/github/stars/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/stargazers) [![Stars](https://img.shields.io/github/stars/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/stargazers)
[![Forks](https://img.shields.io/github/forks/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/network/members) [![Forks](https://img.shields.io/github/forks/affaan-m/ECC?style=flat)](https://github.com/affaan-m/ECC/network/members)
@@ -77,10 +75,30 @@ ECC v2.0.0 adds the public Hermes operator story on top of that reusable layer:
<sub>**OSS stays free.** This repo is MIT-licensed forever. ECC Pro is the hosted GitHub App for private repos. <a href="https://github.com/sponsors/affaan-m">Sponsors</a> and <a href="https://ecc.tools/pricing">Pro subscribers</a> fund the work — that's why a single maintainer ships weekly across 7 harnesses.</sub> <sub>**OSS stays free.** This repo is MIT-licensed forever. ECC Pro is the hosted GitHub App for private repos. <a href="https://github.com/sponsors/affaan-m">Sponsors</a> and <a href="https://ecc.tools/pricing">Pro subscribers</a> fund the work — that's why a single maintainer ships weekly across 7 harnesses.</sub>
<div align="center"> <div align="center">
<sub><strong>Business sponsors</strong></sub><br />
<a href="https://www.coderabbit.ai"><img src="assets/images/sponsors/coderabbit.png" width="72" alt="CodeRabbit logo" /></a> <sub><strong>Business sponsors</strong></sub>
&nbsp;&nbsp;
<a href="https://greptile.com"><img src="assets/images/sponsors/greptile.png" width="72" alt="Greptile logo" /></a> <table>
<tr>
<td align="center" width="220">
<a href="https://www.coderabbit.ai">
<img src="assets/images/sponsors/coderabbit.png" width="96" alt="CodeRabbit logo" /><br />
<strong>CodeRabbit</strong>
</a>
</td>
<td align="center" width="220">
<a href="https://www.greptile.com/go/ecc">
<img src="assets/images/sponsors/greptile.png" width="96" alt="Greptile logo" /><br />
<strong>Greptile</strong>
</a>
</td>
</tr>
</table>
<sub><strong>Community sponsors:</strong> <a href="https://github.com/mikejmorgan-ai">Mike Morgan</a> · <a href="https://github.com/jasonwu513">@jasonwu513</a> · <a href="https://github.com/1anter">@1anter</a> · <a href="https://github.com/massimotodaro">@massimotodaro</a> · <a href="https://github.com/meadmccabe">@meadmccabe</a></sub>
<sub><a href="https://github.com/sponsors/affaan-m"><strong>Become a Sponsor</strong></a> · <a href="SPONSORS.md">Sponsor Tiers</a> · <a href="SPONSORING.md">Sponsorship Program</a></sub>
</div> </div>
--- ---
@@ -91,29 +109,31 @@ This repo is the raw code only. The guides explain everything.
<table> <table>
<tr> <tr>
<td width="33%"> <td width="50%" align="center">
<a href="https://x.com/affaan/status/2012378465664745795"> <a href="./the-shortform-guide.md">
<img src="./assets/images/guides/shorthand-guide.png" alt="The Shorthand Guide to ECC" /> <img src="./assets/images/shortform/00-header.png" width="420" alt="The Shorthand Guide to ECC" /><br />
<b>The Shorthand Guide</b>
</a> </a>
<br /><sub>Setup, foundations, philosophy. <b>Read this first.</b> (<a href="https://x.com/affaan/status/2012378465664745795">thread</a>)</sub>
</td> </td>
<td width="33%"> <td width="50%" align="center">
<a href="https://x.com/affaan/status/2014040193557471352"> <a href="./the-longform-guide.md">
<img src="./assets/images/guides/longform-guide.png" alt="The Longform Guide to ECC" /> <img src="./assets/images/longform/01-header.png" width="420" alt="The Longform Guide to ECC" /><br />
<b>The Longform Guide</b>
</a> </a>
<br /><sub>Token optimization, memory persistence, evals, parallelization. (<a href="https://x.com/affaan/status/2014040193557471352">thread</a>)</sub>
</td> </td>
<td width="33%">
<a href="https://x.com/affaan/status/2033263813387223421">
<img src="./assets/images/security/security-guide-header.png" alt="The Shorthand Guide to Everything Agentic Security" />
</a>
</td>
</tr>
<tr>
<td align="center"><b>Shorthand Guide</b><br/>Setup, foundations, philosophy. <b>Read this first.</b></td>
<td align="center"><b>Longform Guide</b><br/>Token optimization, memory persistence, evals, parallelization.</td>
<td align="center"><b>Security Guide</b><br/>Attack vectors, sandboxing, sanitization, CVEs, AgentShield.</td>
</tr> </tr>
</table> </table>
<div align="center">
<a href="./the-security-guide.md">
<img src="./assets/images/security/security-guide-header.png" width="420" alt="The Shorthand Guide to Everything Agentic Security" /><br />
<b>The Security Guide</b>
</a>
<br /><sub>Attack vectors, sandboxing, sanitization, CVEs, AgentShield. (<a href="https://x.com/affaan/status/2033263813387223421">thread</a>)</sub>
</div>
| Topic | What You'll Learn | | Topic | What You'll Learn |
|-------|-------------------| |-------|-------------------|
| Token Optimization | Model selection, system prompt slimming, background processes | | Token Optimization | Model selection, system prompt slimming, background processes |
@@ -982,10 +1002,12 @@ Use Claude Code's `/mcp` command or CLI-managed MCP setup for live Claude Code s
For repo-local MCP access, copy desired MCP server definitions from `mcp-configs/mcp-servers.json` into a project-scoped `.mcp.json`. For repo-local MCP access, copy desired MCP server definitions from `mcp-configs/mcp-servers.json` into a project-scoped `.mcp.json`.
ECC ships exactly one default connector (`chrome-devtools`); everything else is a skill wrapping a CLI/REST API or an opt-in catalog entry. The rule and the June 2026 audit that retired the previous six defaults live in [docs/MCP-CONNECTOR-POLICY.md](docs/MCP-CONNECTOR-POLICY.md).
If you already run your own copies of ECC-bundled MCPs, set: If you already run your own copies of ECC-bundled MCPs, set:
```bash ```bash
export ECC_DISABLED_MCPS="github,context7,exa,playwright,sequential-thinking,memory" export ECC_DISABLED_MCPS="chrome-devtools"
``` ```
ECC-managed install and Codex sync flows will skip or remove those bundled servers instead of re-adding duplicates. `ECC_DISABLED_MCPS` is an ECC install/sync filter, not a live Claude Code toggle. ECC-managed install and Codex sync flows will skip or remove those bundled servers instead of re-adding duplicates. `ECC_DISABLED_MCPS` is an ECC install/sync filter, not a live Claude Code toggle.
@@ -1769,17 +1791,7 @@ These configs work for my workflow. You should:
## Sponsors ## Sponsors
ECC stays free because paid sponsors fund the work. Featured README placement is reserved for active sponsors. Featured sponsors are at the top of this README — full list and tiers in [SPONSORS.md](SPONSORS.md). [Become a sponsor](https://github.com/sponsors/affaan-m).
<div align="center">
<a href="https://www.coderabbit.ai"><img src="assets/images/sponsors/coderabbit.png" width="80" alt="CodeRabbit logo" /></a>
&nbsp;&nbsp;&nbsp;
<a href="https://greptile.com"><img src="assets/images/sponsors/greptile.png" width="80" alt="Greptile logo" /></a>
<br />
<sub><strong>CodeRabbit</strong> · <strong>Greptile</strong></sub>
</div>
[**Become a Sponsor**](https://github.com/sponsors/affaan-m) | [Sponsor Tiers](SPONSORS.md) | [Sponsorship Program](SPONSORING.md)
--- ---
+1 -1
View File
@@ -11,7 +11,7 @@ Thank you to everyone funding ECC's open-source work. Your sponsorship is what l
| Sponsor | Logo | Since | | Sponsor | Logo | Since |
|---------|------|-------| |---------|------|-------|
| [**CodeRabbit**](https://www.coderabbit.ai) | <img src="assets/images/sponsors/coderabbit.png" width="60" alt="CodeRabbit logo" /> | 2026 | | [**CodeRabbit**](https://www.coderabbit.ai) | <img src="assets/images/sponsors/coderabbit.png" width="60" alt="CodeRabbit logo" /> | 2026 |
| [**Greptile**](https://greptile.com) | <img src="assets/images/sponsors/greptile.png" width="60" alt="Greptile logo" /> | 2026 | | [**Greptile**](https://www.greptile.com/go/ecc) | <img src="assets/images/sponsors/greptile.png" width="60" alt="Greptile logo" /> | 2026 |
*[Become a Business sponsor](https://github.com/sponsors/affaan-m) to get README sponsor placement + SPONSORS.md listing. No seats, SLA, custom development, or preferential technical placement is bundled unless separately agreed.* *[Become a Business sponsor](https://github.com/sponsors/affaan-m) to get README sponsor placement + SPONSORS.md listing. No seats, SLA, custom development, or preferential technical placement is bundled unless separately agreed.*
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 138 KiB

+29
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

+43
View File
@@ -0,0 +1,43 @@
# MCP Connector Policy
ECC ships exactly one default MCP connector. Everything else is a skill wrapping a CLI or REST API, or an opt-in entry in `mcp-configs/mcp-servers.json`.
## The rule
A default connector earns its slot only if both hold:
1. **Universal** — it applies to essentially every user of a coding agent, on every harness ECC targets.
2. **MCP beats a CLI/API wrapped in a skill** — the job genuinely needs what MCP provides: interactive session state, streaming, an auth handshake, or structured browsing. Stateless request/response work is a skill, not a server. Tool schemas load into every session; each default connector taxes every user's context window whether they use it or not.
The default set stays well under ten. In practice the 2026 field default across serious harnesses is zero to two connectors plus native built-ins.
## Current default set
| Server | Why it passes |
|---|---|
| `chrome-devtools` | Google's official DevTools MCP. Interactive CDP sessions — live debugging, performance traces, console and network inspection on a stateful browser. This is the textbook case where MCP beats a CLI: the value is the held-open session, not a one-shot command. Keyless. |
## The six it replaced (June 2026 audit)
| Former default | Verdict | Replacement |
|---|---|---|
| `github` | drop for skill | `gh` CLI via the `github-ops` skill. `gh` is in every model's training data, composes one-shot commands with minimal token overhead, and auths once via `gh auth login`. The MCP server's ~30 tool schemas taxed every session. |
| `context7` | drop for skill | The `documentation-lookup` skill targeting Context7's public REST API (`/api/v2/libs/search`, `/api/v2/context`). Two stateless calls with a bearer key — no session state to justify a server. |
| `exa` | drop for skill | Harness-native search (Claude Code WebSearch, Codex web_search, Cursor @Web) by default; the `exa-search` skill remains for API-key holders. Also required an API key, which fails the universality test for a default. |
| `memory` | drop entirely | Native harness memory (Claude Code auto-memory directories, Cursor memories, AGENTS.md conventions) plus ECC's instinct/continuous-learning system. The knowledge-graph server solved a 2024 problem harnesses have since absorbed. |
| `playwright` | drop for skill | Microsoft's own `@playwright/cli` agent surface — the vendor itself moved agent workflows off MCP because returning full accessibility trees per step burns context. ECC's e2e skills already drive the CLI. Browser *debugging* (the interactive case) is covered by `chrome-devtools`. |
| `sequential-thinking` | drop entirely | Native extended thinking in every modern harness. The server wrapped no external system — a prompting pattern dressed as a connector. |
All six remain available as opt-in entries in `mcp-configs/mcp-servers.json` for users who want them.
## Opt-out
`ECC_DISABLED_MCPS` filters ECC-generated MCP configs at install/sync time:
```bash
export ECC_DISABLED_MCPS="chrome-devtools"
```
## Adding a connector
Open a PR that argues both prongs of the rule explicitly. "Popular" is not an argument; "the job is stateful and universal" is.
+132 -59
View File
@@ -100,6 +100,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "atomic" name = "atomic"
version = "0.6.1" version = "0.6.1"
@@ -144,9 +153,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.0" version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
@@ -163,6 +172,12 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "by_address"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.25.0" version = "1.25.0"
@@ -210,9 +225,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.44" version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
@@ -224,9 +239,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.0" version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -246,9 +261,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.6.0" version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -315,6 +330,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]] [[package]]
name = "cron" name = "cron"
version = "0.12.1" version = "0.12.1"
@@ -332,7 +353,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"crossterm_winapi", "crossterm_winapi",
"mio", "mio",
"parking_lot", "parking_lot",
@@ -348,7 +369,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"crossterm_winapi", "crossterm_winapi",
"derive_more", "derive_more",
"document-features", "document-features",
@@ -591,6 +612,12 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]] [[package]]
name = "filedescriptor" name = "filedescriptor"
version = "0.8.3" version = "0.8.3"
@@ -709,7 +736,7 @@ version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"libc", "libc",
"libgit2-sys", "libgit2-sys",
"log", "log",
@@ -747,6 +774,17 @@ dependencies = [
"foldhash 0.2.0", "foldhash 0.2.0",
] ]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.9.1" version = "0.9.1"
@@ -1012,9 +1050,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.183" version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "libgit2-sys" name = "libgit2-sys"
@@ -1030,6 +1068,12 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.14" version = "0.1.14"
@@ -1082,7 +1126,7 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
] ]
[[package]] [[package]]
@@ -1126,11 +1170,11 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lru" name = "lru"
version = "0.16.3" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
dependencies = [ dependencies = [
"hashbrown 0.16.1", "hashbrown 0.17.1",
] ]
[[package]] [[package]]
@@ -1191,9 +1235,9 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.1.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@@ -1207,7 +1251,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"libc", "libc",
@@ -1323,6 +1367,30 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "palette"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
dependencies = [
"approx",
"fast-srgb8",
"libm",
"palette_derive",
]
[[package]]
name = "palette_derive"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
dependencies = [
"by_address",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@@ -1537,9 +1605,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.30.0" version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" checksum = "1695748e3a735b34968c887ceea5a380b43545903868ae8f5b666593100f6b68"
dependencies = [ dependencies = [
"instability", "instability",
"ratatui-core", "ratatui-core",
@@ -1547,21 +1615,25 @@ dependencies = [
"ratatui-macros", "ratatui-macros",
"ratatui-termwiz", "ratatui-termwiz",
"ratatui-widgets", "ratatui-widgets",
"serde",
] ]
[[package]] [[package]]
name = "ratatui-core" name = "ratatui-core"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" checksum = "42d3603f354bba8c595fa47860e60142d7372b7210c27044c6a7d0e1a4336b44"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"compact_str", "compact_str",
"hashbrown 0.16.1", "critical-section",
"hashbrown 0.17.1",
"indoc", "indoc",
"itertools", "itertools",
"kasuari", "kasuari",
"lru", "lru",
"palette",
"serde",
"strum", "strum",
"thiserror 2.0.18", "thiserror 2.0.18",
"unicode-segmentation", "unicode-segmentation",
@@ -1571,9 +1643,9 @@ dependencies = [
[[package]] [[package]]
name = "ratatui-crossterm" name = "ratatui-crossterm"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" checksum = "2b2867bedcbd6a690ca4f8672a687b730ec07660c79844517b084311b529980c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossterm 0.28.1", "crossterm 0.28.1",
@@ -1584,9 +1656,9 @@ dependencies = [
[[package]] [[package]]
name = "ratatui-macros" name = "ratatui-macros"
version = "0.7.0" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" checksum = "80fac59720679490d89d200df411faa249be728681adcabed3d047ae72c48f1d"
dependencies = [ dependencies = [
"ratatui-core", "ratatui-core",
"ratatui-widgets", "ratatui-widgets",
@@ -1594,9 +1666,9 @@ dependencies = [
[[package]] [[package]]
name = "ratatui-termwiz" name = "ratatui-termwiz"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" checksum = "386b8ff8f74ed749509391c56d549761a2fcdb408e1f42e467286bcb7dac8967"
dependencies = [ dependencies = [
"ratatui-core", "ratatui-core",
"termwiz", "termwiz",
@@ -1604,17 +1676,18 @@ dependencies = [
[[package]] [[package]]
name = "ratatui-widgets" name = "ratatui-widgets"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" checksum = "7ef4f17dd7ac3abf5adc2b920a03c61eee4bfe6a88fa5191936895525371d79c"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"hashbrown 0.16.1", "hashbrown 0.17.1",
"indoc", "indoc",
"instability", "instability",
"itertools", "itertools",
"line-clipping", "line-clipping",
"ratatui-core", "ratatui-core",
"serde",
"strum", "strum",
"time", "time",
"unicode-segmentation", "unicode-segmentation",
@@ -1627,7 +1700,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
] ]
[[package]] [[package]]
@@ -1643,9 +1716,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.3" version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -1666,9 +1739,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.10" version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]] [[package]]
name = "ring" name = "ring"
@@ -1690,7 +1763,7 @@ version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"fallible-iterator", "fallible-iterator",
"fallible-streaming-iterator", "fallible-streaming-iterator",
"hashlink", "hashlink",
@@ -1713,7 +1786,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
@@ -1726,7 +1799,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.12.1", "linux-raw-sys 0.12.1",
@@ -1824,9 +1897,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -1949,18 +2022,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.27.2" version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [ dependencies = [
"strum_macros", "strum_macros",
] ]
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.27.2" version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -2036,7 +2109,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"bitflags 2.11.0", "bitflags 2.13.0",
"fancy-regex", "fancy-regex",
"filedescriptor", "filedescriptor",
"finl_unicode", "finl_unicode",
@@ -2152,9 +2225,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.50.0" version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -2169,9 +2242,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.6.1" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2377,9 +2450,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.22.0" version = "1.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
dependencies = [ dependencies = [
"atomic", "atomic",
"getrandom 0.4.2", "getrandom 0.4.2",
@@ -2511,7 +2584,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.13.0",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"indexmap", "indexmap",
"semver", "semver",
@@ -2846,7 +2919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.11.0", "bitflags 2.13.0",
"indexmap", "indexmap",
"log", "log",
"serde", "serde",
+2 -2
View File
@@ -358,8 +358,8 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@opencode-ai/plugin": "^1.0.0", "@opencode-ai/plugin": "^1.16.2",
"@types/node": "25.7.0", "@types/node": "25.9.2",
"c8": "^11.0.0", "c8": "^11.0.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"globals": "^17.4.0", "globals": "^17.4.0",
+43 -148
View File
@@ -61,10 +61,7 @@ function loadJsonObject(filePath, label) {
assert.fail(`Expected ${label} to contain valid JSON: ${error.message}`); assert.fail(`Expected ${label} to contain valid JSON: ${error.message}`);
} }
assert.ok( assert.ok(parsed && typeof parsed === 'object' && !Array.isArray(parsed), `Expected ${label} to contain a JSON object`);
parsed && typeof parsed === 'object' && !Array.isArray(parsed),
`Expected ${label} to contain a JSON object`,
);
return parsed; return parsed;
} }
@@ -161,34 +158,22 @@ test('.opencode/plugins/ecc-hooks.ts active plugin banner matches package.json',
test('docs/pt-BR/README.md latest release heading matches package.json', () => { test('docs/pt-BR/README.md latest release heading matches package.json', () => {
const source = fs.readFileSync(ptBrReadmePath, 'utf8'); const source = fs.readFileSync(ptBrReadmePath, 'utf8');
assert.ok( assert.ok(source.includes(`### v${expectedVersion} `), 'Expected docs/pt-BR/README.md to advertise the current release heading');
source.includes(`### v${expectedVersion} `),
'Expected docs/pt-BR/README.md to advertise the current release heading',
);
}); });
test('docs/tr/README.md latest release heading matches package.json', () => { test('docs/tr/README.md latest release heading matches package.json', () => {
const source = fs.readFileSync(trReadmePath, 'utf8'); const source = fs.readFileSync(trReadmePath, 'utf8');
assert.ok( assert.ok(source.includes(`### v${expectedVersion} `), 'Expected docs/tr/README.md to advertise the current release heading');
source.includes(`### v${expectedVersion} `),
'Expected docs/tr/README.md to advertise the current release heading',
);
}); });
test('README.zh-CN.md latest release heading matches package.json', () => { test('README.zh-CN.md latest release heading matches package.json', () => {
const source = fs.readFileSync(rootZhCnReadmePath, 'utf8'); const source = fs.readFileSync(rootZhCnReadmePath, 'utf8');
assert.ok( assert.ok(source.includes(`### v${expectedVersion} `), 'Expected README.zh-CN.md to advertise the current release heading');
source.includes(`### v${expectedVersion} `),
'Expected README.zh-CN.md to advertise the current release heading',
);
}); });
test('docs/zh-CN/README.md latest release heading matches package.json', () => { test('docs/zh-CN/README.md latest release heading matches package.json', () => {
const source = fs.readFileSync(zhCnReadmePath, 'utf8'); const source = fs.readFileSync(zhCnReadmePath, 'utf8');
assert.ok( assert.ok(source.includes(`### v${expectedVersion} `), 'Expected docs/zh-CN/README.md to advertise the current release heading');
source.includes(`### v${expectedVersion} `),
'Expected docs/zh-CN/README.md to advertise the current release heading',
);
}); });
// ── Claude plugin manifest ──────────────────────────────────────────────────── // ── Claude plugin manifest ────────────────────────────────────────────────────
@@ -216,10 +201,7 @@ test('claude plugin.json uses short plugin slug', () => {
}); });
test('claude plugin.json does NOT have agents field (unsupported by Claude Code validator)', () => { test('claude plugin.json does NOT have agents field (unsupported by Claude Code validator)', () => {
assert.ok( assert.ok(!('agents' in claudePlugin), 'agents field must NOT be declared — Claude Code plugin validator rejects it');
!('agents' in claudePlugin),
'agents field must NOT be declared — Claude Code plugin validator rejects it',
);
}); });
test('claude plugin.json skills is an array', () => { test('claude plugin.json skills is an array', () => {
@@ -234,26 +216,13 @@ test('claude plugin.json disables bundled MCP servers for provider tool-name com
const legacyPluginName = 'everything-claude-code'; const legacyPluginName = 'everything-claude-code';
const reportedOverlongToolName = `mcp__plugin_${legacyPluginName}_github__create_pull_request_review`; const reportedOverlongToolName = `mcp__plugin_${legacyPluginName}_github__create_pull_request_review`;
assert.ok( assert.ok(reportedOverlongToolName.length > 64, 'Expected the reported GitHub MCP tool name to exceed strict provider limits without the MCP opt-out');
reportedOverlongToolName.length > 64, assert.ok(Object.prototype.hasOwnProperty.call(claudePlugin, 'mcpServers'), 'Expected mcpServers to be explicitly declared so Claude Code does not auto-load root .mcp.json');
'Expected the reported GitHub MCP tool name to exceed strict provider limits without the MCP opt-out', assert.deepStrictEqual(claudePlugin.mcpServers, {}, 'Claude plugin installs must not auto-bundle root MCP servers; document/manual MCP install remains supported');
);
assert.ok(
Object.prototype.hasOwnProperty.call(claudePlugin, 'mcpServers'),
'Expected mcpServers to be explicitly declared so Claude Code does not auto-load root .mcp.json',
);
assert.deepStrictEqual(
claudePlugin.mcpServers,
{},
'Claude plugin installs must not auto-bundle root MCP servers; document/manual MCP install remains supported',
);
}); });
test('claude plugin.json does NOT have explicit hooks declaration', () => { test('claude plugin.json does NOT have explicit hooks declaration', () => {
assert.ok( assert.ok(!('hooks' in claudePlugin), 'hooks field must NOT be declared — Claude Code v2.1+ auto-loads hooks/hooks.json by convention');
!('hooks' in claudePlugin),
'hooks field must NOT be declared — Claude Code v2.1+ auto-loads hooks/hooks.json by convention',
);
}); });
console.log('\n=== .claude-plugin/marketplace.json ===\n'); console.log('\n=== .claude-plugin/marketplace.json ===\n');
@@ -267,10 +236,7 @@ const claudeMarketplace = loadJsonObject(claudeMarketplacePath, '.claude-plugin/
test('claude marketplace.json keeps only Claude-supported top-level keys', () => { test('claude marketplace.json keeps only Claude-supported top-level keys', () => {
const unsupportedTopLevelKeys = ['$schema', 'description']; const unsupportedTopLevelKeys = ['$schema', 'description'];
for (const key of unsupportedTopLevelKeys) { for (const key of unsupportedTopLevelKeys) {
assert.ok( assert.ok(!(key in claudeMarketplace), `.claude-plugin/marketplace.json must not declare unsupported top-level key "${key}"`);
!(key in claudeMarketplace),
`.claude-plugin/marketplace.json must not declare unsupported top-level key "${key}"`,
);
} }
}); });
@@ -316,39 +282,21 @@ test('codex plugin.json version matches package.json', () => {
}); });
test('codex plugin.json skills is a string (not array) per official spec', () => { test('codex plugin.json skills is a string (not array) per official spec', () => {
assert.strictEqual( assert.strictEqual(typeof codexPlugin.skills, 'string', 'skills must be a string path per Codex official docs, not an array');
typeof codexPlugin.skills,
'string',
'skills must be a string path per Codex official docs, not an array',
);
}); });
test('codex plugin.json mcpServers is a string path (not array) per official spec', () => { test('codex plugin.json mcpServers is a string path (not array) per official spec', () => {
assert.strictEqual( assert.strictEqual(typeof codexPlugin.mcpServers, 'string', 'mcpServers must be a string path per Codex official docs');
typeof codexPlugin.mcpServers,
'string',
'mcpServers must be a string path per Codex official docs',
);
}); });
test('codex plugin.json mcpServers exactly matches "./.mcp.json"', () => { test('codex plugin.json mcpServers exactly matches "./.mcp.json"', () => {
assert.strictEqual( assert.strictEqual(codexPlugin.mcpServers, './.mcp.json', 'mcpServers must point exactly to "./.mcp.json" per official docs');
codexPlugin.mcpServers,
'./.mcp.json',
'mcpServers must point exactly to "./.mcp.json" per official docs',
);
const mcpPath = path.join(repoRoot, codexPlugin.mcpServers.replace(/^\.\//, '')); const mcpPath = path.join(repoRoot, codexPlugin.mcpServers.replace(/^\.\//, ''));
assert.ok( assert.ok(fs.existsSync(mcpPath), `mcpServers file missing at plugin root: ${codexPlugin.mcpServers}`);
fs.existsSync(mcpPath),
`mcpServers file missing at plugin root: ${codexPlugin.mcpServers}`,
);
}); });
test('codex plugin.json has interface.displayName', () => { test('codex plugin.json has interface.displayName', () => {
assert.ok( assert.ok(codexPlugin.interface && codexPlugin.interface.displayName, 'Expected interface.displayName for plugin directory presentation');
codexPlugin.interface && codexPlugin.interface.displayName,
'Expected interface.displayName for plugin directory presentation',
);
}); });
test('codex plugin.json uses canonical ECC repo and display name', () => { test('codex plugin.json uses canonical ECC repo and display name', () => {
@@ -363,20 +311,11 @@ test('codex plugin presentation assets exist and ship in npm package', () => {
for (const field of ['composerIcon', 'logo']) { for (const field of ['composerIcon', 'logo']) {
const assetPath = codexPlugin.interface[field]; const assetPath = codexPlugin.interface[field];
assert.ok(assetPath, `Expected interface.${field}`); assert.ok(assetPath, `Expected interface.${field}`);
assert.ok( assert.ok(assetPath.startsWith('./assets/'), `Expected interface.${field} to point at a root assets path, got ${assetPath}`);
assetPath.startsWith('./assets/'),
`Expected interface.${field} to point at a root assets path, got ${assetPath}`,
);
const packagePath = assetPath.replace(/^\.\//, ''); const packagePath = assetPath.replace(/^\.\//, '');
assert.ok( assert.ok(fs.existsSync(path.join(repoRoot, packagePath)), `Expected interface.${field} asset to exist: ${packagePath}`);
fs.existsSync(path.join(repoRoot, packagePath)), assert.ok(packageFiles.has(packagePath), `Expected package.json files to include interface.${field} asset: ${packagePath}`);
`Expected interface.${field} asset to exist: ${packagePath}`,
);
assert.ok(
packageFiles.has(packagePath),
`Expected package.json files to include interface.${field} asset: ${packagePath}`,
);
} }
}); });
@@ -388,31 +327,27 @@ const mcpJsonPath = path.join(repoRoot, '.mcp.json');
test('.mcp.json exists at plugin root (not inside .codex-plugin/)', () => { test('.mcp.json exists at plugin root (not inside .codex-plugin/)', () => {
assert.ok(fs.existsSync(mcpJsonPath), 'Expected .mcp.json at repo root (plugin root)'); assert.ok(fs.existsSync(mcpJsonPath), 'Expected .mcp.json at repo root (plugin root)');
assert.ok( assert.ok(!fs.existsSync(path.join(repoRoot, '.codex-plugin', '.mcp.json')), '.mcp.json must NOT be inside .codex-plugin/ — only plugin.json belongs there');
!fs.existsSync(path.join(repoRoot, '.codex-plugin', '.mcp.json')),
'.mcp.json must NOT be inside .codex-plugin/ — only plugin.json belongs there',
);
}); });
const mcpConfig = loadJsonObject(mcpJsonPath, '.mcp.json'); const mcpConfig = loadJsonObject(mcpJsonPath, '.mcp.json');
test('.mcp.json has mcpServers object', () => { test('.mcp.json has mcpServers object', () => {
assert.ok( assert.ok(mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object', 'Expected mcpServers object');
mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object',
'Expected mcpServers object',
);
}); });
test('.mcp.json includes at least github, context7, and exa servers', () => { test('.mcp.json default set follows the connector policy', () => {
const servers = Object.keys(mcpConfig.mcpServers); const servers = Object.keys(mcpConfig.mcpServers);
assert.ok(servers.includes('github'), 'Expected github MCP server'); assert.ok(servers.includes('chrome-devtools'), 'Expected chrome-devtools as the default browser connector');
assert.ok(servers.includes('context7'), 'Expected context7 MCP server'); assert.ok(servers.length <= 2, `Default connector set must stay minimal per docs/MCP-CONNECTOR-POLICY.md (found ${servers.length})`);
assert.ok(servers.includes('exa'), 'Expected exa MCP server');
}); });
test('.mcp.json declares exa as an http MCP server', () => { test('.mcp.json does not reintroduce retired default connectors', () => {
assert.strictEqual(mcpConfig.mcpServers.exa.type, 'http', 'Expected exa MCP server to declare type=http'); const retired = ['github', 'context7', 'exa', 'memory', 'playwright', 'sequential-thinking'];
assert.strictEqual(mcpConfig.mcpServers.exa.url, 'https://mcp.exa.ai/mcp', 'Expected exa MCP server URL to remain unchanged'); const servers = Object.keys(mcpConfig.mcpServers);
for (const name of retired) {
assert.ok(!servers.includes(name), `${name} was retired from the default set (June 2026 audit) — it lives in mcp-configs/mcp-servers.json as opt-in; see docs/MCP-CONNECTOR-POLICY.md`);
}
}); });
// ── Codex marketplace file ──────────────────────────────────────────────────── // ── Codex marketplace file ────────────────────────────────────────────────────
@@ -422,10 +357,7 @@ console.log('\n=== .agents/plugins/marketplace.json ===\n');
const marketplacePath = path.join(repoRoot, '.agents', 'plugins', 'marketplace.json'); const marketplacePath = path.join(repoRoot, '.agents', 'plugins', 'marketplace.json');
test('marketplace.json exists at .agents/plugins/', () => { test('marketplace.json exists at .agents/plugins/', () => {
assert.ok( assert.ok(fs.existsSync(marketplacePath), 'Expected .agents/plugins/marketplace.json for Codex repo marketplace discovery');
fs.existsSync(marketplacePath),
'Expected .agents/plugins/marketplace.json for Codex repo marketplace discovery',
);
}); });
const marketplace = loadJsonObject(marketplacePath, '.agents/plugins/marketplace.json'); const marketplace = loadJsonObject(marketplacePath, '.agents/plugins/marketplace.json');
@@ -467,24 +399,11 @@ test('marketplace local plugin path resolves to the repo-root Codex bundle', ()
continue; continue;
} }
assert.ok( assert.ok(plugin.source.path.startsWith('./'), `Codex marketplace source.path must be ./-prefixed: ${plugin.source.path}`);
plugin.source.path.startsWith('./'),
`Codex marketplace source.path must be ./-prefixed: ${plugin.source.path}`,
);
const resolvedRoot = path.resolve(repoRoot, plugin.source.path); const resolvedRoot = path.resolve(repoRoot, plugin.source.path);
assert.strictEqual( assert.strictEqual(resolvedRoot, repoRoot, `Expected local marketplace path to resolve to repo root from marketplace root, got: ${plugin.source.path}`);
resolvedRoot, assert.ok(fs.existsSync(path.join(resolvedRoot, '.codex-plugin', 'plugin.json')), `Codex plugin manifest missing under resolved marketplace root: ${plugin.source.path}`);
repoRoot, assert.ok(fs.existsSync(path.join(resolvedRoot, '.mcp.json')), `Root MCP config missing under resolved marketplace root: ${plugin.source.path}`);
`Expected local marketplace path to resolve to repo root from marketplace root, got: ${plugin.source.path}`,
);
assert.ok(
fs.existsSync(path.join(resolvedRoot, '.codex-plugin', 'plugin.json')),
`Codex plugin manifest missing under resolved marketplace root: ${plugin.source.path}`,
);
assert.ok(
fs.existsSync(path.join(resolvedRoot, '.mcp.json')),
`Root MCP config missing under resolved marketplace root: ${plugin.source.path}`,
);
} }
}); });
@@ -510,7 +429,7 @@ test('user-facing docs do not use overlong legacy marketplace install commands',
path.join(repoRoot, 'README.md'), path.join(repoRoot, 'README.md'),
path.join(repoRoot, 'README.zh-CN.md'), path.join(repoRoot, 'README.zh-CN.md'),
path.join(repoRoot, 'skills', 'configure-ecc', 'SKILL.md'), path.join(repoRoot, 'skills', 'configure-ecc', 'SKILL.md'),
...collectMarkdownFiles(path.join(repoRoot, 'docs')), ...collectMarkdownFiles(path.join(repoRoot, 'docs'))
].filter(filePath => !path.relative(repoRoot, filePath).startsWith(`docs${path.sep}drafts${path.sep}`)); ].filter(filePath => !path.relative(repoRoot, filePath).startsWith(`docs${path.sep}drafts${path.sep}`));
const offenders = []; const offenders = [];
@@ -521,19 +440,11 @@ test('user-facing docs do not use overlong legacy marketplace install commands',
} }
} }
assert.deepStrictEqual( assert.deepStrictEqual(offenders, [], `Overlong legacy install commands must not appear in user-facing docs: ${offenders.join(', ')}`);
offenders,
[],
`Overlong legacy install commands must not appear in user-facing docs: ${offenders.join(', ')}`,
);
}); });
test('user-facing docs do not use the legacy non-URL marketplace add form', () => { test('user-facing docs do not use the legacy non-URL marketplace add form', () => {
const markdownFiles = [ const markdownFiles = [path.join(repoRoot, 'README.md'), path.join(repoRoot, 'README.zh-CN.md'), ...collectMarkdownFiles(path.join(repoRoot, 'docs'))];
path.join(repoRoot, 'README.md'),
path.join(repoRoot, 'README.zh-CN.md'),
...collectMarkdownFiles(path.join(repoRoot, 'docs')),
];
const offenders = []; const offenders = [];
for (const filePath of markdownFiles) { for (const filePath of markdownFiles) {
@@ -543,31 +454,15 @@ test('user-facing docs do not use the legacy non-URL marketplace add form', () =
} }
} }
assert.deepStrictEqual( assert.deepStrictEqual(offenders, [], `Legacy non-URL marketplace add form must not appear in user-facing docs: ${offenders.join(', ')}`);
offenders,
[],
`Legacy non-URL marketplace add form must not appear in user-facing docs: ${offenders.join(', ')}`,
);
}); });
test('.codex-plugin README uses current marketplace add flow', () => { test('.codex-plugin README uses current marketplace add flow', () => {
const readme = fs.readFileSync(path.join(repoRoot, '.codex-plugin', 'README.md'), 'utf8'); const readme = fs.readFileSync(path.join(repoRoot, '.codex-plugin', 'README.md'), 'utf8');
assert.ok( assert.ok(readme.includes('codex plugin marketplace add'), 'Expected .codex-plugin README to document codex plugin marketplace add');
readme.includes('codex plugin marketplace add'), assert.ok(readme.includes('codex plugin marketplace add affaan-m/ECC'), 'Expected .codex-plugin README to document the canonical ECC repo marketplace source');
'Expected .codex-plugin README to document codex plugin marketplace add', assert.ok(readme.includes('Official Plugin Directory publishing is coming soon'), 'Expected .codex-plugin README to document current official directory status');
); assert.ok(!/\bcodex plugin install\b/.test(readme), 'codex plugin install is not a current Codex CLI command');
assert.ok(
readme.includes('codex plugin marketplace add affaan-m/ECC'),
'Expected .codex-plugin README to document the canonical ECC repo marketplace source',
);
assert.ok(
readme.includes('Official Plugin Directory publishing is coming soon'),
'Expected .codex-plugin README to document current official directory status',
);
assert.ok(
!/\bcodex plugin install\b/.test(readme),
'codex plugin install is not a current Codex CLI command',
);
}); });
test('docs/zh-CN/README.md version row matches package.json', () => { test('docs/zh-CN/README.md version row matches package.json', () => {
+2 -4
View File
@@ -150,8 +150,7 @@ function runTests() {
const mcpConfig = readJson(path.join(projectDir, '.cursor', 'mcp.json')); const mcpConfig = readJson(path.join(projectDir, '.cursor', 'mcp.json'));
assert.strictEqual(hooksConfig.version, 1); assert.strictEqual(hooksConfig.version, 1);
assert.ok(hooksConfig.hooks.sessionStart, 'Should keep Cursor sessionStart hooks'); assert.ok(hooksConfig.hooks.sessionStart, 'Should keep Cursor sessionStart hooks');
assert.ok(mcpConfig.mcpServers.github, 'Should install shared MCP servers into Cursor'); assert.ok(mcpConfig.mcpServers['chrome-devtools'], 'Should install shared MCP servers into Cursor');
assert.ok(mcpConfig.mcpServers.context7, 'Should include bundled documentation MCPs');
const statePath = path.join(projectDir, '.cursor', 'ecc-install-state.json'); const statePath = path.join(projectDir, '.cursor', 'ecc-install-state.json');
const state = readJson(statePath); const state = readJson(statePath);
@@ -194,8 +193,7 @@ function runTests() {
const mcpConfig = readJson(path.join(projectDir, '.cursor', 'mcp.json')); const mcpConfig = readJson(path.join(projectDir, '.cursor', 'mcp.json'));
assert.ok(mcpConfig.mcpServers.custom, 'Should preserve existing custom Cursor MCP servers'); assert.ok(mcpConfig.mcpServers.custom, 'Should preserve existing custom Cursor MCP servers');
assert.ok(mcpConfig.mcpServers.github, 'Should merge bundled GitHub MCP server'); assert.ok(mcpConfig.mcpServers['chrome-devtools'], 'Should merge the bundled chrome-devtools MCP server');
assert.ok(mcpConfig.mcpServers.playwright, 'Should merge bundled Playwright MCP server');
} finally { } finally {
cleanup(homeDir); cleanup(homeDir);
cleanup(projectDir); cleanup(projectDir);
+1 -1
View File
@@ -211,7 +211,7 @@ git worktree add ../feature-branch feature-branch
Stream and watch logs/bash processes Claude runs: Stream and watch logs/bash processes Claude runs:
<https://github.com/user-attachments/assets/shortform/07-tmux-video.mp4> [Watch: tmux session streaming a long-running command (video)](./assets/images/shortform/07-tmux-video.mp4)
```bash ```bash
tmux new -s dev tmux new -s dev
+82 -82
View File
@@ -178,59 +178,59 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.3": "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.4":
version: 3.0.3 version: 3.0.4
resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.3" resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.4"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.3": "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.4":
version: 3.0.3 version: 3.0.4
resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.3" resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.4"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.3": "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.4":
version: 3.0.3 version: 3.0.4
resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.3" resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.4"
conditions: os=linux & cpu=arm64 conditions: os=linux & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.3": "@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.4":
version: 3.0.3 version: 3.0.4
resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.3" resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.4"
conditions: os=linux & cpu=arm conditions: os=linux & cpu=arm
languageName: node languageName: node
linkType: hard linkType: hard
"@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.3": "@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.4":
version: 3.0.3 version: 3.0.4
resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.3" resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.4"
conditions: os=linux & cpu=x64 conditions: os=linux & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.3": "@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.4":
version: 3.0.3 version: 3.0.4
resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.3" resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.4"
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"@opencode-ai/plugin@npm:^1.0.0": "@opencode-ai/plugin@npm:^1.16.2":
version: 1.15.3 version: 1.16.2
resolution: "@opencode-ai/plugin@npm:1.15.3" resolution: "@opencode-ai/plugin@npm:1.16.2"
dependencies: dependencies:
"@opencode-ai/sdk": "npm:1.15.3" "@opencode-ai/sdk": "npm:1.16.2"
effect: "npm:4.0.0-beta.65" effect: "npm:4.0.0-beta.74"
zod: "npm:4.1.8" zod: "npm:4.1.8"
peerDependencies: peerDependencies:
"@opentui/core": ">=0.2.11" "@opentui/core": ">=0.3.2"
"@opentui/keymap": ">=0.2.11" "@opentui/keymap": ">=0.3.2"
"@opentui/solid": ">=0.2.11" "@opentui/solid": ">=0.3.2"
peerDependenciesMeta: peerDependenciesMeta:
"@opentui/core": "@opentui/core":
optional: true optional: true
@@ -238,16 +238,16 @@ __metadata:
optional: true optional: true
"@opentui/solid": "@opentui/solid":
optional: true optional: true
checksum: 10c0/8f680d7e318e3a8b64314c9923e00ecbbde33b195651b59ca4bbfb7cbf0a90a5ea30df7c806ec9611482fef96dfd6b48432267af6df3bde068535774a97d5fe9 checksum: 10c0/a10d6259557c2b587afd93dc690bf5b3b0132272c0b0bca1f77c5a7ea9e3440ebbf1fbdf0a24e4a20ae05f23961c546a2feab53c8f50b10a9d237090c41d2146
languageName: node languageName: node
linkType: hard linkType: hard
"@opencode-ai/sdk@npm:1.15.3": "@opencode-ai/sdk@npm:1.16.2":
version: 1.15.3 version: 1.16.2
resolution: "@opencode-ai/sdk@npm:1.15.3" resolution: "@opencode-ai/sdk@npm:1.16.2"
dependencies: dependencies:
cross-spawn: "npm:7.0.6" cross-spawn: "npm:7.0.6"
checksum: 10c0/b9345d31fa53f2f22cb1a63e7fe39d3e260ea2d0ca65ae27525c203beefd5db0ce7854eeaa5054b2cf1680164f19cc9e90c2e049dfac9cdc804a4b53a9128a4c checksum: 10c0/0f8de5a64bfb4411ecddd0bb2c3049df07a5661a05e7c965e2404692f80394652b00a4d46985e97aabf8b26ac85e4313d8dbf7bce2280a297eb4fa88bf700a6a
languageName: node languageName: node
linkType: hard linkType: hard
@@ -302,12 +302,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:25.7.0": "@types/node@npm:25.9.2":
version: 25.7.0 version: 25.9.2
resolution: "@types/node@npm:25.7.0" resolution: "@types/node@npm:25.9.2"
dependencies: dependencies:
undici-types: "npm:~7.21.0" undici-types: "npm:>=7.24.0 <7.24.7"
checksum: 10c0/47ec7eaca154c36ad6d1ac0270e6e254eedf20b9dc49afe3bc76e4f7eba29ceac705f8903b162aeaf40e3941101ffe76ffb374989359ea3ef8c8509d8b443f55 checksum: 10c0/f14c0d56361febb985eccc45cf0834ee6e2f07c4389a636f3e1a55ebde320077a80bface18c9afd3092f5fa295925502c1a9d55f805efa813f634aa9c941cbac
languageName: node languageName: node
linkType: hard linkType: hard
@@ -631,8 +631,8 @@ __metadata:
dependencies: dependencies:
"@eslint/js": "npm:^9.39.2" "@eslint/js": "npm:^9.39.2"
"@iarna/toml": "npm:^2.2.5" "@iarna/toml": "npm:^2.2.5"
"@opencode-ai/plugin": "npm:^1.0.0" "@opencode-ai/plugin": "npm:^1.16.2"
"@types/node": "npm:25.7.0" "@types/node": "npm:25.9.2"
ajv: "npm:^8.18.0" ajv: "npm:^8.18.0"
c8: "npm:^11.0.0" c8: "npm:^11.0.0"
eslint: "npm:^9.39.2" eslint: "npm:^9.39.2"
@@ -647,21 +647,21 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"effect@npm:4.0.0-beta.65": "effect@npm:4.0.0-beta.74":
version: 4.0.0-beta.65 version: 4.0.0-beta.74
resolution: "effect@npm:4.0.0-beta.65" resolution: "effect@npm:4.0.0-beta.74"
dependencies: dependencies:
"@standard-schema/spec": "npm:^1.1.0" "@standard-schema/spec": "npm:^1.1.0"
fast-check: "npm:^4.6.0" fast-check: "npm:^4.8.0"
find-my-way-ts: "npm:^0.1.6" find-my-way-ts: "npm:^0.1.6"
ini: "npm:^6.0.0" ini: "npm:^7.0.0"
kubernetes-types: "npm:^1.30.0" kubernetes-types: "npm:^1.30.0"
msgpackr: "npm:^1.11.9" msgpackr: "npm:^2.0.1"
multipasta: "npm:^0.2.7" multipasta: "npm:^0.2.7"
toml: "npm:^4.1.1" toml: "npm:^4.1.1"
uuid: "npm:^13.0.0" uuid: "npm:^14.0.0"
yaml: "npm:^2.8.3" yaml: "npm:^2.9.0"
checksum: 10c0/ed89d877461414af2bbfccc0c7b66a3f5a46d08e7e7d8b10bec9f969db97180b916e8c079138dab985937c8e9297fa003ef2d1d560458924cd2308203baf305c checksum: 10c0/3dfc7ce7b58bbe9e8459ea9eba0abdcef7d9e7082643c64f4cc5165632777aa163f20d7b5f86372f819e6b60154e44cea125abb71be01da667a330c10a9b5892
languageName: node languageName: node
linkType: hard linkType: hard
@@ -823,12 +823,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fast-check@npm:^4.6.0": "fast-check@npm:^4.8.0":
version: 4.7.0 version: 4.8.0
resolution: "fast-check@npm:4.7.0" resolution: "fast-check@npm:4.8.0"
dependencies: dependencies:
pure-rand: "npm:^8.0.0" pure-rand: "npm:^8.0.0"
checksum: 10c0/7edce2b82d11d5325e9e79a2377e1f6e7200d27219edda2e3449d827e994c34461132fc149c90e41b78fc8e6ef4aae77d45350ac7bb1bc4a81110401d0a49fbc checksum: 10c0/f72556a29db4ff386a8b6e50d420b06c7e5eaafff7db5560a99136c57d8d4777998155eb02d1bbeff396f575cc0b1442c8a1c4ddb798c4a919b542de1a1904ff
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1025,10 +1025,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ini@npm:^6.0.0": "ini@npm:^7.0.0":
version: 6.0.0 version: 7.0.0
resolution: "ini@npm:6.0.0" resolution: "ini@npm:7.0.0"
checksum: 10c0/9a7f55f306e2b25b41ae67c8b526e8f4673f057b70852b9025816ef4f15f07bf1ba35ed68ea4471ff7b31718f7ef1bc50d709f8d03cb012e10a3135eb99c7206 checksum: 10c0/7520cae38bd5587e1cbca4637bb5fc0e157f38e0816522756456010ef703772d0018f9f4987cb9977bf3b10c1feccaad7800744dd1f5d85fb435e58a1baa9754
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1671,16 +1671,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"msgpackr-extract@npm:^3.0.2": "msgpackr-extract@npm:^3.0.4":
version: 3.0.3 version: 3.0.4
resolution: "msgpackr-extract@npm:3.0.3" resolution: "msgpackr-extract@npm:3.0.4"
dependencies: dependencies:
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "npm:3.0.3" "@msgpackr-extract/msgpackr-extract-darwin-arm64": "npm:3.0.4"
"@msgpackr-extract/msgpackr-extract-darwin-x64": "npm:3.0.3" "@msgpackr-extract/msgpackr-extract-darwin-x64": "npm:3.0.4"
"@msgpackr-extract/msgpackr-extract-linux-arm": "npm:3.0.3" "@msgpackr-extract/msgpackr-extract-linux-arm": "npm:3.0.4"
"@msgpackr-extract/msgpackr-extract-linux-arm64": "npm:3.0.3" "@msgpackr-extract/msgpackr-extract-linux-arm64": "npm:3.0.4"
"@msgpackr-extract/msgpackr-extract-linux-x64": "npm:3.0.3" "@msgpackr-extract/msgpackr-extract-linux-x64": "npm:3.0.4"
"@msgpackr-extract/msgpackr-extract-win32-x64": "npm:3.0.3" "@msgpackr-extract/msgpackr-extract-win32-x64": "npm:3.0.4"
node-gyp: "npm:latest" node-gyp: "npm:latest"
node-gyp-build-optional-packages: "npm:5.2.2" node-gyp-build-optional-packages: "npm:5.2.2"
dependenciesMeta: dependenciesMeta:
@@ -1698,19 +1698,19 @@ __metadata:
optional: true optional: true
bin: bin:
download-msgpackr-prebuilds: bin/download-prebuilds.js download-msgpackr-prebuilds: bin/download-prebuilds.js
checksum: 10c0/e504fd8bf86a29d7527c83776530ee6dc92dcb0273bb3679fd4a85173efead7f0ee32fb82c8410a13c33ef32828c45f81118ffc0fbed5d6842e72299894623b4 checksum: 10c0/582a9d17abbf3019e600e948736695056280ce401fd0235ee2474e95f9952208b9f6cce4d0e355b03b7d3c5630e6c3d11fe5fc27fdedb2311cce48de464338d8
languageName: node languageName: node
linkType: hard linkType: hard
"msgpackr@npm:^1.11.9": "msgpackr@npm:^2.0.1":
version: 1.11.12 version: 2.0.4
resolution: "msgpackr@npm:1.11.12" resolution: "msgpackr@npm:2.0.4"
dependencies: dependencies:
msgpackr-extract: "npm:^3.0.2" msgpackr-extract: "npm:^3.0.4"
dependenciesMeta: dependenciesMeta:
msgpackr-extract: msgpackr-extract:
optional: true optional: true
checksum: 10c0/e9f1460e363dbd8c81a5c1b5829980edea7d76e91d570d094d0a4dae0d8ad12f64dea11b2be15f3d7b48d615fa9d3c9b600a6894fd272526087fa33753b5fd16 checksum: 10c0/b72e8de59ce82a29cd5ca6d93ceec589367dd742a75fb04b6d26d6e0c5863f1000d0b27979469f18ca4554f3ccabe620524013268d5387415bcab495cca4ae73
languageName: node languageName: node
linkType: hard linkType: hard
@@ -2117,10 +2117,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"undici-types@npm:~7.21.0": "undici-types@npm:>=7.24.0 <7.24.7":
version: 7.21.0 version: 7.24.6
resolution: "undici-types@npm:7.21.0" resolution: "undici-types@npm:7.24.6"
checksum: 10c0/c3b4ae5f066c398acb1962505b56214ecd72843f7d7827fcc2df7a48a63d1639d3608c580ac09f836253d21fa7ba8f1a04440569ed9d332474ad01b8a010db87 checksum: 10c0/d9cd8befb643ac904615c280a095ba4240531f6bb4a5e75a22a7483630ca8d3f1016d2ab6ace6ceda1f63b3a2db2fe037fafe121d6917a0187573aa548ff78ca
languageName: node languageName: node
linkType: hard linkType: hard
@@ -2140,12 +2140,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"uuid@npm:^13.0.0": "uuid@npm:^14.0.0":
version: 13.0.1 version: 14.0.0
resolution: "uuid@npm:13.0.1" resolution: "uuid@npm:14.0.0"
bin: bin:
uuid: dist-node/bin/uuid uuid: dist-node/bin/uuid
checksum: 10c0/7bb8ad18b11871b7bd1b9161a60610c2b6ce8f7300d93932f92117a2ab9b40479dd23e81929ac848e8a7c45f78b8ed3333f88694b71c17ff3265e443f8684642 checksum: 10c0/a57ae7794c45005c1a9208989196c5baf79a7679c30f43c1bee9033a2c4d113a2cea216fa6fcc9663b08b0d55635df1a7c6eb7e7f3d21c3e50688c698fa39a50
languageName: node languageName: node
linkType: hard linkType: hard
@@ -2214,12 +2214,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"yaml@npm:^2.8.3": "yaml@npm:^2.9.0":
version: 2.8.4 version: 2.9.0
resolution: "yaml@npm:2.8.4" resolution: "yaml@npm:2.9.0"
bin: bin:
yaml: bin.mjs yaml: bin.mjs
checksum: 10c0/0a33a1fa28d4bc79f61a12ec7ef7a2bce0ce5f8e80c6eaecfb4a0c88c08767dd1ede372b6a3bcd70891213b8c9f3169b355c97e77026d3b3459e10d2cccaef1e checksum: 10c0/f340718df45e97a9551b9bf9dac61c80050bc464513b710debfb5067c380c8472e3b67809cffacb4ab5ffb5e66ef9310816c88b05f371cec60abfedd8c88e0a2
languageName: node languageName: node
linkType: hard linkType: hard