mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-11 18:46:05 +00:00
refactor: route Grok installs through canonical adapter
This commit is contained in:
1 parent
0724e2d8bc
commit
f69f945398
55 files changed
+1132
-2220
No files matched your search
+19
-10
@@ -11,10 +11,19 @@ thin subdirectory copy drops parent-relative runtime files.
|
||||
|
||||
## Install
|
||||
|
||||
ECC trusted install is `node scripts/grok-install.js` (`previewInstall` /
|
||||
`applyInstall` in `scripts/lib/grok-harness-adapter.js`, install target `grok`,
|
||||
state at `~/.grok/ecc/install-state.json`). That plan is receipt-backed and
|
||||
requires explicit consent for hooks and each MCP server.
|
||||
ECC installs through the canonical installer and receipt lifecycle:
|
||||
|
||||
```bash
|
||||
node scripts/grok-install.js --dry-run --trust --consent-hooks --consent-mcp chrome-devtools
|
||||
node scripts/grok-install.js --trust --consent-hooks --consent-mcp chrome-devtools
|
||||
node scripts/repair.js --target grok
|
||||
node scripts/uninstall.js --target grok
|
||||
```
|
||||
|
||||
`scripts/grok-install.js` is a thin alias for `install-apply.js --target grok
|
||||
--profile full`; state stays at `~/.grok/ecc/install-state.json`. Upgrade and
|
||||
rollback use the same canonical apply path; pass `--source-sha <40-hex>` to
|
||||
reapply a previous pinned commit.
|
||||
|
||||
Grok CLI marketplace add / install / enable is **discovery only**.
|
||||
`grok plugin install --trust` skips Grok's confirmation prompt; it is **not
|
||||
@@ -39,14 +48,14 @@ grok plugin validate /absolute/path/to/ECC
|
||||
|
||||
Do not also run `./install.sh --target claude` (or a full manual Claude copy)
|
||||
into the same Grok session. Pick one install path per harness.
|
||||
`./install.sh --target grok` does not copy hooks or root MCP without the same
|
||||
adapter consent flags.
|
||||
`./install.sh --target grok --profile full` does not copy hooks or root MCP
|
||||
without `--trust` and the matching consent flags.
|
||||
|
||||
## Hooks
|
||||
|
||||
Grok sets `GROK_PLUGIN_ROOT`. Shared hook code does not read that alias.
|
||||
`applyInstall` maps it onto `PLUGIN_ROOT` at the installed-tree boundary when
|
||||
hooks are consented. Without hook consent, the installed copy has no
|
||||
The Grok target adapter maps it onto the shared root contract in the installed
|
||||
hook configuration when hooks are consented. Without hook consent, the installed copy has no
|
||||
`hooks/hooks.json`.
|
||||
|
||||
Grok does not honor Claude `userConfig`. Hook enablement and profile stay on
|
||||
@@ -62,8 +71,8 @@ export ECC_HOOK_PROFILE=standard # minimal | standard | strict
|
||||
`.grok-plugin/plugin.json` sets `"mcpServers": ""` so a trusted Grok plugin
|
||||
install does not attach repo-root `.mcp.json` or `chrome-devtools`. That root
|
||||
file remains for Claude/Codex. Additional connectors stay opt-in via
|
||||
`mcp-configs/mcp-servers.json`. Adapter receipts still require explicit
|
||||
per-server consent before any MCP name is copied into an install plan.
|
||||
`mcp-configs/mcp-servers.json`. Canonical receipts record explicit per-server
|
||||
consent before any MCP name is copied into an install plan.
|
||||
|
||||
## Known limits
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"description": "Harness-native ECC operator layer - 68 agents, 286 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Grok Build, Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
"email": "me@affaanmustafa.com"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Harness-native ECC plugin for engineering teams - 68 agents, 286 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Grok Build plus adjacent agent harnesses",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
|
||||
@@ -11,7 +11,7 @@ Update ECC from its upstream repo and regenerate the current context's managed i
|
||||
|
||||
```bash
|
||||
# Preview the update without mutating anything
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/auto-update.js" --dry-run
|
||||
|
||||
# Update only Cursor-managed files in the current project
|
||||
|
||||
@@ -18,7 +18,7 @@ while a legacy `~/.claude/skills/continuous-learning-v2/` directory still
|
||||
exists (#2037).
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status
|
||||
```
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Use `/sessions info` when you need operator-surface context for a swarm: branch,
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const sm = require(_r + '/scripts/lib/session-manager');
|
||||
const aa = require(_r + '/scripts/lib/session-aliases');
|
||||
const path = require('path');
|
||||
@@ -71,7 +71,7 @@ Load and display a session's content (by ID or alias).
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const sm = require(_r + '/scripts/lib/session-manager');
|
||||
const aa = require(_r + '/scripts/lib/session-aliases');
|
||||
const id = process.argv[1];
|
||||
@@ -145,7 +145,7 @@ Create a memorable alias for a session.
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const sm = require(_r + '/scripts/lib/session-manager');
|
||||
const aa = require(_r + '/scripts/lib/session-aliases');
|
||||
|
||||
@@ -186,7 +186,7 @@ Delete an existing alias.
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const aa = require(_r + '/scripts/lib/session-aliases');
|
||||
|
||||
const aliasName = process.argv[1];
|
||||
@@ -216,7 +216,7 @@ Show detailed information about a session.
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const sm = require(_r + '/scripts/lib/session-manager');
|
||||
const aa = require(_r + '/scripts/lib/session-aliases');
|
||||
|
||||
@@ -267,7 +267,7 @@ Show all session aliases.
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const _r = (function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();
|
||||
const aa = require(_r + '/scripts/lib/session-aliases');
|
||||
|
||||
const aliases = aa.listAliases();
|
||||
|
||||
@@ -13,21 +13,21 @@ Shows a comprehensive health dashboard for all skills in the portfolio with succ
|
||||
Run the skill health CLI in dashboard mode:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard
|
||||
```
|
||||
|
||||
For a specific panel only:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard --panel failures
|
||||
```
|
||||
|
||||
For machine-readable output:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard --json
|
||||
```
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ The matrix below is rendered from
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Claude Code | Native | Claude plugin assets; skills; commands; hooks; MCP config; local rules; statusline-oriented workflows | Claude-native hooks do not imply parity in other harnesses | `./install.sh --profile minimal --target claude`; Claude plugin install | `npm run harness:audit -- --format json`; `node scripts/session-inspect.js --list-adapters` | Avoid loading every skill by default; keep hooks opt-in and inspectable. |
|
||||
| Codex | Instruction-backed | `AGENTS.md`; Codex plugin metadata; skills; MCP reference config; command patterns | Native hook enforcement and Claude slash-command semantics are not equivalent | `./install.sh --profile minimal --target codex`; repo-local `AGENTS.md` review | `npm run harness:audit -- --format json` | Treat hooks as policy text unless a native Codex hook surface exists. |
|
||||
| Grok Build | Adapter-backed | Grok plugin and marketplace manifests; shared skills; commands; hooks only after adapter hook consent; MCP opted out on the native plugin surface (`mcpServers: ""` and `hooks: ""`) | Claude userConfig hook profiles are ignored; use ECC_HOOK_PROFILE; ECC agent tool allowlists use Claude names and are not mapped in Grok agent frontmatter; No plugin rules/ surface | `node scripts/grok-install.js --dry-run` / `node scripts/grok-install.js` (ECC 2 `grok` install-state under ~/.grok/ecc/); `grok plugin marketplace add affaan-m/ECC && grok plugin install ecc && grok plugin enable ecc` (discovery only; not ECC consent) | `grok plugin validate .`; `node tests/lib/grok-harness-adapter.test.js`; `node tests/plugin-manifest.test.js` | Grok rejects Claude marketplace source "./"; pin a url+sha catalog in .grok-plugin/marketplace.json to a commit that contains the Grok adapter; ECC consent is grok-install.js / applyInstall; native Grok CLI discovery opts hooks and MCP out via empty plugin.json fields; the full skill catalog is large |
|
||||
| Grok Build | Adapter-backed | Grok plugin and marketplace manifests; shared skills; commands; hooks only after adapter hook consent; MCP opted out on the native plugin surface (`mcpServers: ""` and `hooks: ""`) | Claude userConfig hook profiles are ignored; use ECC_HOOK_PROFILE; ECC agent tool allowlists use Claude names and are not mapped in Grok agent frontmatter; No plugin rules/ surface | `node scripts/grok-install.js --dry-run` / `node scripts/grok-install.js` (canonical `grok` install-state under ~/.grok/ecc/); `grok plugin marketplace add affaan-m/ECC && grok plugin install ecc && grok plugin enable ecc` (discovery only; not ECC consent) | `grok plugin validate .`; `node tests/lib/grok-harness-adapter.test.js`; `node tests/plugin-manifest.test.js` | Grok rejects Claude marketplace source "./"; pin a url+sha catalog in .grok-plugin/marketplace.json to a commit that contains the Grok adapter; ECC consent is the canonical install-apply receipt path; native Grok CLI discovery opts hooks and MCP out via empty plugin.json fields; the full skill catalog is large |
|
||||
| OpenCode | Adapter-backed | OpenCode package/plugin metadata; shared skills; MCP config; event adapter patterns | Event names, plugin packaging, and command dispatch differ from Claude Code | OpenCode package or plugin surface from this repo | `node tests/scripts/build-opencode.test.js`; `npm run harness:audit -- --format json` | Keep hook logic in shared scripts and adapt only event shape at the edge. |
|
||||
| Pi | Adapter-backed | Pi package manifest; canonical ECC skills (skills/); canonical ECC commands as prompt templates (commands/); canonical ECC engineering rules (rules/common/) injected into the system prompt; session lifecycle hook adapter; /ecc-doctor diagnostics command; canonical ECC agents (agents/) portable via scripts/agent-convert.js (claude -> pi) | Agent chains, approval prompts, and persistent todos require companion Pi packages and are not part of this adapter (individual agent definitions are portable via scripts/agent-convert.js); Pi core has no MCP surface, though ECC MCP configs load verbatim through the community pi-mcp-adapter package, which ECC neither installs nor depends on | `pi install git:github.com/affaan-m/ECC`; `pi install /path/to/ECC` from a local checkout | `node tests/pi/pi-package-manifest.test.js`; `node tests/pi/pi-extension-adapter.test.js`; `node tests/pi/agent-convert.test.js`; `npm run harness:adapters -- --check` | Pi extensions execute with full user permissions, and hooks run without a shell and resolve from the installed package rather than the user project; Keep canonical skills and commands as the single source of truth, and never generate copies under .pi/ |
|
||||
| Cursor | Adapter-backed | Cursor rules; project-local skills; hook adapter; shared scripts | Cursor hook events and rule loading differ from Claude Code | `./install.sh --profile minimal --target cursor` | `node tests/lib/install-targets.test.js`; `npm run harness:audit -- --format json` | Cursor adapters must preserve existing project rules and avoid silent overwrite. |
|
||||
|
||||
@@ -11,7 +11,7 @@ ECCをアップストリームリポジトリから更新し、元のインス
|
||||
|
||||
```bash
|
||||
# 何も変更せずに更新をプレビュー
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/auto-update.js" --dry-run
|
||||
|
||||
# 現在のプロジェクトのCursor管理ファイルのみ更新
|
||||
|
||||
@@ -18,7 +18,7 @@ command: true
|
||||
残っているときに発生するパスの分岐を回避します (#2037)。
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status
|
||||
```
|
||||
|
||||
|
||||
@@ -13,21 +13,21 @@ command: true
|
||||
ダッシュボードモードでスキルヘルスCLIを実行:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard
|
||||
```
|
||||
|
||||
特定のパネルのみ:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard --panel failures
|
||||
```
|
||||
|
||||
機械読み取り可能な出力:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard --json
|
||||
```
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Bu, `CLAUDE_PLUGIN_ROOT` ayarlanmamışken eski bir
|
||||
yol sapmasını önler (#2037).
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status
|
||||
```
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ Bir swarm için operatör-yüzey context'e ihtiyacınız olduğunda `/sessions i
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const path = require('path');
|
||||
|
||||
const result = sm.getAllSessions({ limit: 20 });
|
||||
@@ -70,8 +70,8 @@ Session içeriğini yükle ve göster (ID veya alias ile).
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const id = process.argv[1];
|
||||
|
||||
// Önce alias olarak çözümlemeyi dene
|
||||
@@ -143,8 +143,8 @@ Session için akılda kalıcı bir alias oluştur.
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
|
||||
const sessionId = process.argv[1];
|
||||
const aliasName = process.argv[2];
|
||||
@@ -183,7 +183,7 @@ Mevcut bir alias'ı sil.
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
|
||||
const aliasName = process.argv[1];
|
||||
if (!aliasName) {
|
||||
@@ -222,7 +222,7 @@ Tüm session aliaslarını göster.
|
||||
**Script:**
|
||||
```bash
|
||||
node -e "
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
|
||||
const aliases = aa.listAliases();
|
||||
console.log('Session Aliases (' + aliases.length + '):');
|
||||
|
||||
@@ -11,7 +11,7 @@ disable-model-invocation: true
|
||||
|
||||
```bash
|
||||
# Preview the update without mutating anything
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/auto-update.js" --dry-run
|
||||
|
||||
# Update only Cursor-managed files in the current project
|
||||
|
||||
@@ -16,7 +16,7 @@ command: true
|
||||
`~/.claude/skills/continuous-learning-v2/` 目录仍然存在时发生的路径分歧 (#2037)。
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status
|
||||
```
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ description: 管理Claude Code会话历史、别名和会话元数据。
|
||||
|
||||
```bash
|
||||
node -e "
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const path = require('path');
|
||||
|
||||
const result = sm.getAllSessions({ limit: 20 });
|
||||
@@ -72,8 +72,8 @@ for (const s of result.sessions) {
|
||||
|
||||
```bash
|
||||
node -e "
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const id = process.argv[1];
|
||||
|
||||
// First try to resolve as alias
|
||||
@@ -146,8 +146,8 @@ if (session.metadata.worktree) {
|
||||
|
||||
```bash
|
||||
node -e "
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
|
||||
const sessionId = process.argv[1];
|
||||
const aliasName = process.argv[2];
|
||||
@@ -187,7 +187,7 @@ if (result.success) {
|
||||
|
||||
```bash
|
||||
node -e "
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
|
||||
const aliasName = process.argv[1];
|
||||
if (!aliasName) {
|
||||
@@ -217,8 +217,8 @@ if (result.success) {
|
||||
|
||||
```bash
|
||||
node -e "
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const sm = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-manager');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
|
||||
const id = process.argv[1];
|
||||
const resolved = aa.resolveAlias(id);
|
||||
@@ -268,7 +268,7 @@ if (aliases.length > 0) {
|
||||
|
||||
```bash
|
||||
node -e "
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
const aa = require((function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()+'/scripts/lib/session-aliases');
|
||||
|
||||
const aliases = aa.listAliases();
|
||||
console.log('Session Aliases (' + aliases.length + '):');
|
||||
|
||||
@@ -13,21 +13,21 @@ command: true
|
||||
在仪表盘模式下运行技能健康 CLI:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard
|
||||
```
|
||||
|
||||
仅针对特定面板:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard --panel failures
|
||||
```
|
||||
|
||||
获取机器可读输出:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${GROK_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}}"
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
|
||||
node "$ECC_ROOT/scripts/skills-health.js" --dashboard --json
|
||||
```
|
||||
|
||||
|
||||
@@ -105,12 +105,6 @@ Remove or comment out the hook entry in `hooks.json`. If installed as a plugin,
|
||||
}
|
||||
```
|
||||
|
||||
Grok Build plugin-root layout and `GROK_PLUGIN_ROOT` live in
|
||||
`scripts/lib/grok-harness-adapter.js`. Shared hook runners stay on
|
||||
`PLUGIN_ROOT` / `CLAUDE_PLUGIN_ROOT` / `ECC_PLUGIN_ROOT`. Grok does not honor
|
||||
Claude `userConfig`; use the runtime variables below (`ECC_HOOK_PROFILE`,
|
||||
`ECC_HOOKS_ENABLED`) after explicit hook consent.
|
||||
|
||||
### Runtime Hook Controls (Recommended)
|
||||
|
||||
Use environment variables to control hook behavior without editing `hooks.json`:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"if(!process.env.PLUGIN_ROOT)throw new Error('Missing Codex PLUGIN_ROOT');process.env.CLAUDE_PLUGIN_ROOT=process.env.PLUGIN_ROOT;const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/session-start-bootstrap.js"
|
||||
"command": "node -e \"if(!process.env.PLUGIN_ROOT)throw new Error('Missing Codex PLUGIN_ROOT');process.env.CLAUDE_PLUGIN_ROOT=process.env.PLUGIN_ROOT;const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/session-start-bootstrap.js"
|
||||
}
|
||||
],
|
||||
"description": "Load previous context and detect package manager on new session",
|
||||
|
||||
+23
-23
@@ -6,7 +6,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/pre-bash-dispatcher.js"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/pre-bash-dispatcher.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:write:doc-file-warning scripts/hooks/doc-file-warning.js standard,strict"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:write:doc-file-warning scripts/hooks/doc-file-warning.js standard,strict"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -34,7 +34,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:edit-write:suggest-compact scripts/hooks/suggest-compact.js standard,strict"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:edit-write:suggest-compact scripts/hooks/suggest-compact.js standard,strict"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:observe scripts/hooks/observe-runner.js standard,strict",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:observe scripts/hooks/observe-runner.js standard,strict",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
@@ -54,7 +54,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:governance-capture scripts/hooks/governance-capture.js standard,strict",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:governance-capture scripts/hooks/governance-capture.js standard,strict",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
@@ -64,7 +64,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:config-protection scripts/hooks/config-protection.js standard,strict",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:config-protection scripts/hooks/config-protection.js standard,strict",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
@@ -93,7 +93,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:mcp-health-check scripts/hooks/mcp-health-check.js standard,strict"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:mcp-health-check scripts/hooks/mcp-health-check.js standard,strict"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -103,7 +103,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:edit-write:gateguard-fact-force scripts/hooks/gateguard-fact-force.js standard,strict",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:edit-write:gateguard-fact-force scripts/hooks/gateguard-fact-force.js standard,strict",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
@@ -115,7 +115,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:compact scripts/hooks/pre-compact.js standard,strict"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:compact scripts/hooks/pre-compact.js standard,strict"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -126,7 +126,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/session-start-bootstrap.js"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/session-start-bootstrap.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -135,7 +135,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js session-start:plan-canvas-sessions scripts/hooks/plan-canvas-sessions.js standard,strict"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js session-start:plan-canvas-sessions scripts/hooks/plan-canvas-sessions.js standard,strict"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -146,7 +146,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/posttooluse-dispatcher.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.env.ECC_POSTTOOLUSE_PASSTHROUGH='1';process.argv.splice(1,0,s);require(s).cli()\" sync",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/posttooluse-dispatcher.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.env.ECC_POSTTOOLUSE_PASSTHROUGH='1';process.argv.splice(1,0,s);require(s).cli()\" sync",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
@@ -156,7 +156,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/posttooluse-dispatcher.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.env.ECC_POSTTOOLUSE_PASSTHROUGH='1';process.argv.splice(1,0,s);require(s).cli()\" async",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/posttooluse-dispatcher.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.env.ECC_POSTTOOLUSE_PASSTHROUGH='1';process.argv.splice(1,0,s);require(s).cli()\" async",
|
||||
"async": true,
|
||||
"timeout": 45
|
||||
}
|
||||
@@ -169,7 +169,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:mcp-health-check scripts/hooks/mcp-health-check.js standard,strict"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:mcp-health-check scripts/hooks/mcp-health-check.js standard,strict"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -178,7 +178,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:skill:track scripts/hooks/skill-run-tracker.js standard,strict"
|
||||
"command": "node -e \"const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:skill:track scripts/hooks/skill-run-tracker.js standard,strict"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -189,7 +189,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:plan-canvas-pending','scripts/hooks/plan-canvas-pending.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\""
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:plan-canvas-pending','scripts/hooks/plan-canvas-pending.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -198,7 +198,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:format-typecheck','scripts/hooks/stop-format-typecheck.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:300000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:format-typecheck','scripts/hooks/stop-format-typecheck.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:300000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"timeout": 300
|
||||
}
|
||||
]
|
||||
@@ -208,7 +208,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:check-console-log','scripts/hooks/check-console-log.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\""
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:check-console-log','scripts/hooks/check-console-log.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -217,7 +217,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:session-end','scripts/hooks/session-end.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:session-end','scripts/hooks/session-end.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
@@ -228,7 +228,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:evaluate-session','scripts/hooks/evaluate-session.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:evaluate-session','scripts/hooks/evaluate-session.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
@@ -239,7 +239,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:cost-tracker','scripts/hooks/cost-tracker.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:cost-tracker','scripts/hooks/cost-tracker.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
@@ -250,7 +250,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:desktop-notify','scripts/hooks/desktop-notify.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const finish=(out,err,code)=>{let pending=1;const done=()=>{pending-=1;if(pending===0)process.exit(code);};if(out){pending+=1;process.stdout.write(out,done);}if(err){pending+=1;process.stderr.write(err,done);}process.nextTick(done);};const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'stop:desktop-notify','scripts/hooks/desktop-notify.js','standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000,maxBuffer:16*1024*1024});const failed=result.error||result.status===null||result.signal;const stdout=!failed&&typeof result.stdout==='string'?result.stdout:'';let stderr=typeof result.stderr==='string'?result.stderr:'';let code=Number.isInteger(result.status)?result.status:0;if(failed){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');stderr+='[Stop] ERROR: hook runner failed: '+reason+String.fromCharCode(10);code=1;}finish(stdout,stderr,code);}else{finish(raw,'[Stop] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10),0);}\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
@@ -263,7 +263,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'session:end:marker','scripts/hooks/session-end-marker.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[SessionEnd] ERROR: hook runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[SessionEnd] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\"",
|
||||
"command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const rel=path.join('scripts','hooks','run-with-flags.js');const root=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,'session:end:marker','scripts/hooks/session-end-marker.js','minimal,standard,strict'],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[SessionEnd] ERROR: hook runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[SessionEnd] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\"",
|
||||
"async": true,
|
||||
"timeout": 10
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"hermes",
|
||||
"openclaw",
|
||||
"kimi",
|
||||
"grok",
|
||||
"adal"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -114,6 +114,16 @@
|
||||
"declined",
|
||||
null
|
||||
]
|
||||
},
|
||||
"trust": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"consentMcp": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -156,6 +166,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"repoUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"repoCommit": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -25,8 +25,6 @@ const DOCS_ZH_CN_README_PATH = path.join(ROOT, 'docs', 'zh-CN', 'README.md');
|
||||
const DOCS_ZH_CN_AGENTS_PATH = path.join(ROOT, 'docs', 'zh-CN', 'AGENTS.md');
|
||||
const PLUGIN_JSON_PATH = path.join(ROOT, '.claude-plugin', 'plugin.json');
|
||||
const MARKETPLACE_JSON_PATH = path.join(ROOT, '.claude-plugin', 'marketplace.json');
|
||||
const GROK_PLUGIN_JSON_PATH = path.join(ROOT, '.grok-plugin', 'plugin.json');
|
||||
const GROK_MARKETPLACE_JSON_PATH = path.join(ROOT, '.grok-plugin', 'marketplace.json');
|
||||
const WRITE_MODE = process.argv.includes('--write');
|
||||
|
||||
const OUTPUT_MODE = process.argv.includes('--md')
|
||||
@@ -649,8 +647,6 @@ function createDocumentSpecs(paths = {}) {
|
||||
zhDocsAgentsPath = DOCS_ZH_CN_AGENTS_PATH,
|
||||
pluginJsonPath = PLUGIN_JSON_PATH,
|
||||
marketplaceJsonPath = MARKETPLACE_JSON_PATH,
|
||||
grokPluginJsonPath = GROK_PLUGIN_JSON_PATH,
|
||||
grokMarketplaceJsonPath = GROK_MARKETPLACE_JSON_PATH,
|
||||
} = paths;
|
||||
|
||||
return [
|
||||
@@ -719,36 +715,6 @@ function createDocumentSpecs(paths = {}) {
|
||||
(parsed, description) => { parsed.plugins[0].description = description; }
|
||||
),
|
||||
},
|
||||
{
|
||||
filePath: grokPluginJsonPath,
|
||||
parseExpectations: content => parseCatalogDescriptionExpectations(
|
||||
content,
|
||||
'.grok-plugin/plugin.json description',
|
||||
parsed => parsed.description
|
||||
),
|
||||
syncContent: (content, catalog) => syncCatalogDescription(
|
||||
content,
|
||||
catalog,
|
||||
'.grok-plugin/plugin.json description',
|
||||
parsed => parsed.description,
|
||||
(parsed, description) => { parsed.description = description; }
|
||||
),
|
||||
},
|
||||
{
|
||||
filePath: grokMarketplaceJsonPath,
|
||||
parseExpectations: content => parseCatalogDescriptionExpectations(
|
||||
content,
|
||||
'.grok-plugin/marketplace.json plugin description',
|
||||
parsed => parsed.plugins?.[0]?.description
|
||||
),
|
||||
syncContent: (content, catalog) => syncCatalogDescription(
|
||||
content,
|
||||
catalog,
|
||||
'.grok-plugin/marketplace.json plugin description',
|
||||
parsed => parsed.plugins?.[0]?.description,
|
||||
(parsed, description) => { parsed.plugins[0].description = description; }
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -763,8 +729,6 @@ function createDocumentSpecsForRoot(root) {
|
||||
zhDocsAgentsPath: path.join(root, 'docs', 'zh-CN', 'AGENTS.md'),
|
||||
pluginJsonPath: path.join(root, '.claude-plugin', 'plugin.json'),
|
||||
marketplaceJsonPath: path.join(root, '.claude-plugin', 'marketplace.json'),
|
||||
grokPluginJsonPath: path.join(root, '.grok-plugin', 'plugin.json'),
|
||||
grokMarketplaceJsonPath: path.join(root, '.grok-plugin', 'marketplace.json'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+20
-108
@@ -2,126 +2,38 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const {
|
||||
parseMcpConsentList,
|
||||
resolveHomeDir,
|
||||
runGrokInstall,
|
||||
} = require('./lib/grok-harness-adapter');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const IGNORED_VALUE_FLAGS = new Set([
|
||||
'--target',
|
||||
'--profile',
|
||||
'--modules',
|
||||
'--with',
|
||||
'--without',
|
||||
'--config',
|
||||
'--locale',
|
||||
'--skill',
|
||||
'--skills',
|
||||
]);
|
||||
const SELECTION_FLAGS = new Set(['--profile', '--modules', '--with', '--skill', '--skills', '--config']);
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: node scripts/grok-install.js [--dry-run] [--json] [--trust] [--consent-hooks] [--consent-mcp <name,name>]
|
||||
|
||||
ECC trusted Grok install. This is the consent/receipt path.
|
||||
Grok CLI marketplace add/install/enable is discovery only.
|
||||
|
||||
Options:
|
||||
--dry-run Preview the plan without copying files
|
||||
--json Emit JSON
|
||||
--trust Mark the plan trusted (still requires per-capability consent)
|
||||
--consent-hooks Consent to hooks
|
||||
--consent-mcp Comma-separated MCP server names to attach
|
||||
--help Show this help
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const parsed = {
|
||||
dryRun: false,
|
||||
json: false,
|
||||
trust: false,
|
||||
help: false,
|
||||
consent: { hooks: false, mcp: {} },
|
||||
};
|
||||
|
||||
for (let index = 2; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === '--dry-run') {
|
||||
parsed.dryRun = true;
|
||||
} else if (arg === '--json') {
|
||||
parsed.json = true;
|
||||
} else if (arg === '--trust') {
|
||||
parsed.trust = true;
|
||||
} else if (arg === '--consent-hooks') {
|
||||
parsed.consent.hooks = true;
|
||||
} else if (arg === '--consent-mcp') {
|
||||
parsed.consent.mcp = parseMcpConsentList(argv[index + 1] || '');
|
||||
function canonicalArgs(argv = process.argv.slice(2)) {
|
||||
const args = [];
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
if (argv[index] === '--target') {
|
||||
index += 1;
|
||||
} else if (IGNORED_VALUE_FLAGS.has(arg)) {
|
||||
index += 1;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
parsed.help = true;
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
continue;
|
||||
}
|
||||
args.push(argv[index]);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printHuman(result) {
|
||||
const plan = result.plan;
|
||||
console.log(result.dryRun ? 'Grok adapter dry-run plan\n' : 'Grok adapter install\n');
|
||||
console.log(`Identity: ${plan.identity}`);
|
||||
console.log(`Trust: ${plan.trust}`);
|
||||
console.log(`Hooks enabled: ${plan.hooksEnabled}`);
|
||||
console.log(`MCP attached: ${plan.mcpAttached.join(', ') || '(none)'}`);
|
||||
console.log(`Chrome DevTools: ${plan.attachChromeDevtools}`);
|
||||
console.log(`Native MCP opted out: ${result.nativeMcpOptedOut}`);
|
||||
console.log(`Native hooks opted out: ${result.nativeHooksOptedOut}`);
|
||||
if (result.receipt) {
|
||||
console.log(`Receipt: ${result.receipt.id}`);
|
||||
console.log(`Install-state: ${result.receipt.installStatePath}`);
|
||||
console.log(`Installed root: ${result.receipt.installedRoot}`);
|
||||
if (!args.some((argument) => SELECTION_FLAGS.has(argument)) && !args.includes('--help') && !args.includes('-h')) {
|
||||
args.unshift('--profile', 'full');
|
||||
}
|
||||
return ['--target', 'grok', ...args];
|
||||
}
|
||||
|
||||
function main() {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseArgs(process.argv);
|
||||
} catch (error) {
|
||||
process.stderr.write(`Error: ${error.message}\n`);
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (parsed.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = runGrokInstall({
|
||||
dryRun: parsed.dryRun,
|
||||
trust: parsed.trust,
|
||||
consent: parsed.consent,
|
||||
homeDir: resolveHomeDir(),
|
||||
repoRoot: path.join(__dirname, '..'),
|
||||
});
|
||||
|
||||
if (parsed.json) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
printHuman(result);
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(__dirname, 'install-apply.js'), ...canonicalArgs()],
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
process.exitCode = result.status === null ? 1 : result.status;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
if (require.main === module) main();
|
||||
|
||||
module.exports = {
|
||||
parseArgs,
|
||||
canonicalArgs,
|
||||
main,
|
||||
};
|
||||
@@ -13,9 +13,6 @@ function getPluginRoot(options = {}) {
|
||||
if (options.pluginRoot && String(options.pluginRoot).trim()) {
|
||||
return String(options.pluginRoot).trim();
|
||||
}
|
||||
if (process.env.GROK_PLUGIN_ROOT && process.env.GROK_PLUGIN_ROOT.trim()) {
|
||||
return process.env.GROK_PLUGIN_ROOT.trim();
|
||||
}
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT && process.env.CLAUDE_PLUGIN_ROOT.trim()) {
|
||||
return process.env.CLAUDE_PLUGIN_ROOT.trim();
|
||||
}
|
||||
@@ -136,7 +133,6 @@ function run(raw, options = {}) {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
GROK_PLUGIN_ROOT: pluginRoot,
|
||||
CLAUDE_PLUGIN_ROOT: pluginRoot,
|
||||
ECC_PLUGIN_ROOT: pluginRoot
|
||||
},
|
||||
|
||||
@@ -173,7 +173,6 @@ function spawnNode(rootDir, relPath, raw, args, options = {}) {
|
||||
ensureAgentDataHomeEnv();
|
||||
const hookEnv = {
|
||||
...process.env,
|
||||
GROK_PLUGIN_ROOT: rootDir,
|
||||
CLAUDE_PLUGIN_ROOT: rootDir,
|
||||
ECC_PLUGIN_ROOT: rootDir,
|
||||
ECC_HOOK_INPUT_MAX_BYTES: String(options.maxStdin),
|
||||
@@ -207,7 +206,6 @@ function spawnShell(rootDir, relPath, raw, args, options = {}) {
|
||||
ensureAgentDataHomeEnv();
|
||||
const hookEnv = {
|
||||
...process.env,
|
||||
GROK_PLUGIN_ROOT: rootDir,
|
||||
CLAUDE_PLUGIN_ROOT: rootDir,
|
||||
ECC_PLUGIN_ROOT: rootDir,
|
||||
ECC_HOOK_INPUT_MAX_BYTES: String(options.maxStdin),
|
||||
@@ -268,7 +266,7 @@ async function main() {
|
||||
contextScanner.push(contextDecoder.end());
|
||||
const hookContext = contextScanner.context;
|
||||
const rootDir = normalizePluginRootForPlatform(
|
||||
process.env.GROK_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT
|
||||
process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT
|
||||
);
|
||||
|
||||
if (!mode || !relPath || !rootDir) {
|
||||
|
||||
@@ -69,7 +69,7 @@ const ASYNC_HOOKS = [
|
||||
];
|
||||
|
||||
function getPluginRoot(env = process.env) {
|
||||
return env.GROK_PLUGIN_ROOT || env.CLAUDE_PLUGIN_ROOT || env.ECC_PLUGIN_ROOT || path.resolve(__dirname, '..', '..');
|
||||
return env.CLAUDE_PLUGIN_ROOT || env.ECC_PLUGIN_ROOT || path.resolve(__dirname, '..', '..');
|
||||
}
|
||||
|
||||
function matchesTool(matcher, toolName) {
|
||||
|
||||
@@ -142,9 +142,6 @@ function truncatedInputResult(hookId, maxStdin) {
|
||||
}
|
||||
|
||||
function getPluginRoot() {
|
||||
if (process.env.GROK_PLUGIN_ROOT && process.env.GROK_PLUGIN_ROOT.trim()) {
|
||||
return process.env.GROK_PLUGIN_ROOT;
|
||||
}
|
||||
if (process.env.CLAUDE_PLUGIN_ROOT && process.env.CLAUDE_PLUGIN_ROOT.trim()) {
|
||||
return process.env.CLAUDE_PLUGIN_ROOT;
|
||||
}
|
||||
@@ -322,7 +319,6 @@ async function main() {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
GROK_PLUGIN_ROOT: pluginRoot,
|
||||
CLAUDE_PLUGIN_ROOT: pluginRoot,
|
||||
ECC_PLUGIN_ROOT: pluginRoot,
|
||||
ECC_HOOK_ID: hookId,
|
||||
|
||||
+13
-58
@@ -21,16 +21,6 @@ const { getComputeSponsorCopy } = require('./lib/compute-sponsor');
|
||||
const { stripAnsi } = require('./lib/utils');
|
||||
const { describeMissingDependencyError } = require('./lib/missing-dependency');
|
||||
|
||||
function argvTarget(argv) {
|
||||
const args = argv.slice(2);
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
if (args[index] === '--target') {
|
||||
return args[index + 1] || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getHelpText() {
|
||||
const languages = listLegacyCompatibilityLanguages();
|
||||
const locales = listSupportedLocales();
|
||||
@@ -58,7 +48,7 @@ Targets:
|
||||
zed - Install project settings, commands, agents, skills, and flattened rules into ./.zed/
|
||||
hermes - Install shared rules/skills/commands into ~/.hermes/
|
||||
kimi - Install Kimi Code project instructions, skills, and MCP config into ./.kimi-code/ (ECC hooks not configured)
|
||||
grok - ECC Grok adapter install (previewInstall/applyInstall). Discovery-only Grok CLI is separate.
|
||||
grok - Install through the canonical plan/apply/receipt lifecycle into ~/.grok/plugins/ecc/
|
||||
openclaw - Install shared rules/skills/commands into ~/.openclaw/
|
||||
adal - Install shared rules/skills/commands into ./.adal/
|
||||
|
||||
@@ -75,6 +65,10 @@ Options:
|
||||
--enable-hooks Confirm installing the automatic hook runtime (required
|
||||
when the selected profile/modules materialize hooks)
|
||||
--no-hooks Install everything except the automatic hook runtime
|
||||
--trust Trust executable Grok capabilities (still requires each consent flag)
|
||||
--consent-hooks Allow Grok hooks for this install
|
||||
--consent-mcp <ids> Allow named Grok MCP servers, comma-separated
|
||||
--source-sha <sha> Install or roll back Grok to an exact 40-character Git commit
|
||||
--dry-run Show the install plan without copying files
|
||||
--json Emit machine-readable plan/result JSON
|
||||
--help Show this help text
|
||||
@@ -166,46 +160,8 @@ function printHumanPlan(plan, dryRun) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let rawPlan = null;
|
||||
try {
|
||||
if (argvTarget(process.argv) === 'grok') {
|
||||
const { parseArgs } = require('./grok-install');
|
||||
const { parseMcpConsentList, resolveHomeDir, runGrokInstall } = require('./lib/grok-harness-adapter');
|
||||
const parsed = parseArgs(process.argv);
|
||||
if (parsed.help) {
|
||||
showHelp(0);
|
||||
}
|
||||
const grokResult = runGrokInstall({
|
||||
dryRun: parsed.dryRun === true,
|
||||
trust: parsed.trust === true || process.env.ECC_GROK_TRUST === '1',
|
||||
consent: {
|
||||
hooks: parsed.consent.hooks === true || process.env.ECC_GROK_CONSENT_HOOKS === '1',
|
||||
mcp: {
|
||||
...parseMcpConsentList(process.env.ECC_GROK_CONSENT_MCP || ''),
|
||||
...parsed.consent.mcp,
|
||||
},
|
||||
},
|
||||
homeDir: resolveHomeDir(),
|
||||
repoRoot: process.cwd(),
|
||||
});
|
||||
const options = { dryRun: parsed.dryRun, json: parsed.json };
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ dryRun: options.dryRun === true, grok: grokResult }, null, 2));
|
||||
} else {
|
||||
console.log(options.dryRun ? 'Grok adapter dry-run plan\n' : 'Grok adapter install\n');
|
||||
console.log(`Identity: ${grokResult.plan.identity}`);
|
||||
console.log(`Trust: ${grokResult.plan.trust}`);
|
||||
console.log(`Hooks enabled: ${grokResult.plan.hooksEnabled}`);
|
||||
console.log(`MCP attached: ${grokResult.plan.mcpAttached.join(', ') || '(none)'}`);
|
||||
console.log(`Chrome DevTools: ${grokResult.plan.attachChromeDevtools}`);
|
||||
if (grokResult.receipt) {
|
||||
console.log(`Install-state: ${grokResult.receipt.installStatePath}`);
|
||||
}
|
||||
console.log('\nConsent flags: ECC_GROK_TRUST=1 ECC_GROK_CONSENT_HOOKS=1 ECC_GROK_CONSENT_MCP=name,name');
|
||||
console.log('Or: node scripts/grok-install.js --dry-run --trust --consent-hooks');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const options = parseInstallArgs(process.argv);
|
||||
|
||||
if (options.help) {
|
||||
@@ -232,7 +188,7 @@ async function main() {
|
||||
config,
|
||||
});
|
||||
|
||||
const rawPlan = createInstallPlanFromRequest(request, {
|
||||
rawPlan = createInstallPlanFromRequest(request, {
|
||||
projectRoot: process.cwd(),
|
||||
homeDir: process.env.HOME || os.homedir(),
|
||||
env: process.env,
|
||||
@@ -267,13 +223,12 @@ async function main() {
|
||||
printHumanPlan(result, false);
|
||||
}
|
||||
} catch (error) {
|
||||
const missingDependencyMessage = describeMissingDependencyError(error);
|
||||
process.stderr.write(
|
||||
missingDependencyMessage
|
||||
? `Error: ${missingDependencyMessage}\n`
|
||||
: `Error: ${error.message}${getHelpText()}`
|
||||
);
|
||||
process.exit(1);
|
||||
process.stderr.write(`Error: ${error.message}${getHelpText()}`);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (rawPlan && typeof rawPlan.dispose === 'function') {
|
||||
rawPlan.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,35 +4,13 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
createInstallState,
|
||||
readInstallState,
|
||||
writeInstallState,
|
||||
} = require('./install-state');
|
||||
const grokHomeTarget = require('./install-targets/grok-home');
|
||||
const {
|
||||
isGitWorkTreeRoot,
|
||||
resolvePinnedGitSha,
|
||||
copyInstallSource,
|
||||
assertSourceIdentity,
|
||||
writePinnedMarketplace,
|
||||
} = require('./grok-source-identity');
|
||||
const { SHA_PATTERN } = require('./grok-source-identity');
|
||||
|
||||
const GROK_PLUGIN_ROOT_ENV = 'GROK_PLUGIN_ROOT';
|
||||
const GROK_HOME_DIRNAME = '.grok';
|
||||
const INSTALLED_PLUGINS_DIR = 'installed-plugins';
|
||||
const PREVIOUS_STATE_FILE = 'install-state.previous.json';
|
||||
const SOURCE_IDENTITY_FILE = '.ecc-source.json';
|
||||
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
||||
const HOOKS_CAPABILITY_ID = 'hooks';
|
||||
const CHROME_DEVTOOLS_MCP = 'chrome-devtools';
|
||||
const CURRENT_PLUGIN_SLUG = 'ecc';
|
||||
const PLUGIN_CACHE_SLUGS = [CURRENT_PLUGIN_SLUG, 'everything-claude-code'];
|
||||
const PLUGIN_ROOT_SEGMENTS = [
|
||||
[CURRENT_PLUGIN_SLUG],
|
||||
[`${CURRENT_PLUGIN_SLUG}@${CURRENT_PLUGIN_SLUG}`],
|
||||
['marketplaces', CURRENT_PLUGIN_SLUG],
|
||||
];
|
||||
const DEFAULT_SCRIPT_PROBE = path.join('scripts', 'lib', 'utils.js');
|
||||
const DEFAULT_SKILL_PROBE = path.join('skills', 'continuous-learning-v2');
|
||||
const SHARED_PLUGIN_ROOT_KEYS = Object.freeze([
|
||||
@@ -49,163 +27,29 @@ function grokPluginRootFromEnv(env = {}) {
|
||||
return trimEnv(env[GROK_PLUGIN_ROOT_ENV]);
|
||||
}
|
||||
|
||||
function hasSharedPluginRoot(env = {}) {
|
||||
return SHARED_PLUGIN_ROOT_KEYS.some((key) => trimEnv(env[key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Grok-only env onto the harness-neutral PLUGIN_ROOT without leaving
|
||||
* GROK_PLUGIN_ROOT for shared hook/root code to select.
|
||||
*/
|
||||
function toSharedPluginEnv(env = {}) {
|
||||
const next = { ...env };
|
||||
const grokRoot = grokPluginRootFromEnv(env);
|
||||
delete next[GROK_PLUGIN_ROOT_ENV];
|
||||
if (grokRoot && !hasSharedPluginRoot(next)) {
|
||||
if (grokRoot && !SHARED_PLUGIN_ROOT_KEYS.some((key) => trimEnv(next[key]))) {
|
||||
next.PLUGIN_ROOT = grokRoot;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grok-facing plugin.json `mcpServers: ""` opts the native trusted plugin
|
||||
* surface out of repo-root `.mcp.json` (including chrome-devtools).
|
||||
*/
|
||||
function nativePluginMcpOptedOut(pluginManifest = {}) {
|
||||
const value = pluginManifest.mcpServers;
|
||||
if (value === '' || value === undefined || value === null) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return Object.keys(value).length === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function nativeTrustedMcpServerNames(pluginManifest = {}) {
|
||||
if (nativePluginMcpOptedOut(pluginManifest)) {
|
||||
return [];
|
||||
}
|
||||
if (pluginManifest.mcpServers && typeof pluginManifest.mcpServers === 'object'
|
||||
&& !Array.isArray(pluginManifest.mcpServers)) {
|
||||
return Object.keys(pluginManifest.mcpServers);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function nativeTrustedInstallAttachesChromeDevtools(pluginManifest = {}) {
|
||||
return nativeTrustedMcpServerNames(pluginManifest).includes(CHROME_DEVTOOLS_MCP);
|
||||
}
|
||||
|
||||
function readGrokPluginManifest(pluginJsonPath, readFile = (file) => fs.readFileSync(file, 'utf8')) {
|
||||
return JSON.parse(readFile(pluginJsonPath));
|
||||
}
|
||||
|
||||
function resolveHomeDir(env = process.env) {
|
||||
const explicit = trimEnv(env.ECC_GROK_HOME);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
return os.homedir();
|
||||
return trimEnv(env.ECC_GROK_HOME) || os.homedir();
|
||||
}
|
||||
|
||||
function grokHomeDir(homeDir, pathModule = path) {
|
||||
if (pathModule === path) {
|
||||
return grokHomeTarget.resolveRoot({ homeDir });
|
||||
}
|
||||
return pathModule.join(homeDir, GROK_HOME_DIRNAME);
|
||||
}
|
||||
|
||||
function grokInstallStatePath(homeDir, pathModule = path) {
|
||||
if (pathModule === path) {
|
||||
return grokHomeTarget.getInstallStatePath({ homeDir });
|
||||
}
|
||||
if (pathModule === path) return grokHomeTarget.getInstallStatePath({ homeDir });
|
||||
return pathModule.join(homeDir, GROK_HOME_DIRNAME, 'ecc', 'install-state.json');
|
||||
}
|
||||
|
||||
function grokPreviousStatePath(homeDir, pathModule = path) {
|
||||
return pathModule.join(pathModule.dirname(grokInstallStatePath(homeDir, pathModule)), PREVIOUS_STATE_FILE);
|
||||
}
|
||||
|
||||
function normalizeContainedPath(candidate, pathModule = path) {
|
||||
const raw = String(candidate || '');
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
return pathModule.normalize(raw);
|
||||
}
|
||||
|
||||
function isContained(candidate, root, pathModule = path) {
|
||||
const resolvedRoot = pathModule.resolve(normalizeContainedPath(root, pathModule));
|
||||
const resolvedCandidate = pathModule.resolve(resolvedRoot, normalizeContainedPath(candidate, pathModule));
|
||||
if (resolvedCandidate === resolvedRoot) {
|
||||
return true;
|
||||
}
|
||||
const prefix = resolvedRoot.endsWith(pathModule.sep)
|
||||
? resolvedRoot
|
||||
: resolvedRoot + pathModule.sep;
|
||||
return resolvedCandidate.startsWith(prefix);
|
||||
}
|
||||
|
||||
function assertRootContainment(candidate, root, pathModule = path) {
|
||||
if (!isContained(candidate, root, pathModule)) {
|
||||
throw new Error(`install path escapes Grok root: ${candidate}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function assertPinnedSource(source) {
|
||||
if (!source || typeof source !== 'object') {
|
||||
throw new Error('marketplace source must be an object');
|
||||
}
|
||||
if (source.source !== 'url') {
|
||||
throw new Error('marketplace source must be type url');
|
||||
}
|
||||
if (!source.url || typeof source.url !== 'string') {
|
||||
throw new Error('marketplace source must include a Git URL');
|
||||
}
|
||||
if (!SHA_PATTERN.test(source.sha || '')) {
|
||||
throw new Error('marketplace source must pin a 40-character lowercase commit SHA');
|
||||
}
|
||||
return {
|
||||
source: 'url',
|
||||
url: source.url,
|
||||
sha: source.sha,
|
||||
};
|
||||
}
|
||||
|
||||
function sourceIdentity(source) {
|
||||
const pinned = assertPinnedSource(source);
|
||||
return `${pinned.url}@${pinned.sha}`;
|
||||
}
|
||||
|
||||
function readMarketplaceSource(catalogPath, readFile = (file) => fs.readFileSync(file, 'utf8')) {
|
||||
const catalog = JSON.parse(readFile(catalogPath));
|
||||
const plugin = catalog && Array.isArray(catalog.plugins) ? catalog.plugins[0] : null;
|
||||
if (!plugin) {
|
||||
throw new Error('marketplace catalog has no plugins');
|
||||
}
|
||||
return {
|
||||
name: plugin.name,
|
||||
version: plugin.version,
|
||||
source: assertPinnedSource(plugin.source),
|
||||
};
|
||||
}
|
||||
|
||||
function listMcpCapabilities(mcpConfig) {
|
||||
const servers = mcpConfig && mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object'
|
||||
? mcpConfig.mcpServers
|
||||
: {};
|
||||
return Object.keys(servers).map((name) => ({
|
||||
id: name,
|
||||
kind: 'mcp',
|
||||
command: servers[name] && servers[name].command,
|
||||
}));
|
||||
}
|
||||
|
||||
function readJsonIfPresent(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
@@ -215,310 +59,25 @@ function readJsonIfPresent(filePath) {
|
||||
}
|
||||
|
||||
function isCompleteRoot(dir, probe, pathModule = path) {
|
||||
if (probe) {
|
||||
return fs.existsSync(pathModule.join(dir, probe));
|
||||
}
|
||||
if (probe) return fs.existsSync(pathModule.join(dir, probe));
|
||||
return fs.existsSync(pathModule.join(dir, DEFAULT_SCRIPT_PROBE))
|
||||
&& fs.existsSync(pathModule.join(dir, DEFAULT_SKILL_PROBE));
|
||||
}
|
||||
|
||||
function findRootInGrokHome(homeDir, probe, pathModule = path) {
|
||||
const vendorDir = grokHomeDir(homeDir, pathModule);
|
||||
if (isCompleteRoot(vendorDir, probe, pathModule)) {
|
||||
return vendorDir;
|
||||
}
|
||||
|
||||
for (const segments of PLUGIN_ROOT_SEGMENTS) {
|
||||
const candidate = pathModule.join(vendorDir, 'plugins', ...segments);
|
||||
if (isCompleteRoot(candidate, probe, pathModule)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (const slug of PLUGIN_CACHE_SLUGS) {
|
||||
const cacheBase = pathModule.join(vendorDir, 'plugins', 'cache', slug);
|
||||
const orgDirs = fs.readdirSync(cacheBase, { withFileTypes: true });
|
||||
for (const orgEntry of orgDirs) {
|
||||
if (!orgEntry.isDirectory()) continue;
|
||||
const orgPath = pathModule.join(cacheBase, orgEntry.name);
|
||||
let versionDirs;
|
||||
try {
|
||||
versionDirs = fs.readdirSync(orgPath, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const verEntry of versionDirs) {
|
||||
if (!verEntry.isDirectory()) continue;
|
||||
const candidate = pathModule.join(orgPath, verEntry.name);
|
||||
if (isCompleteRoot(candidate, probe, pathModule)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Grok plugin cache is optional.
|
||||
}
|
||||
|
||||
try {
|
||||
const installedBase = pathModule.join(vendorDir, INSTALLED_PLUGINS_DIR);
|
||||
const installedDirs = fs.readdirSync(installedBase, { withFileTypes: true });
|
||||
for (const entry of installedDirs) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const candidate = pathModule.join(installedBase, entry.name);
|
||||
if (isCompleteRoot(candidate, probe, pathModule)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// installed-plugins is Grok-specific and optional.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function receiptIdFromState(state) {
|
||||
const sha = state && state.source && state.source.repoCommit
|
||||
? String(state.source.repoCommit).slice(0, 12)
|
||||
: 'unknown';
|
||||
return `ecc-${sha}-${state.installedAt}`;
|
||||
}
|
||||
|
||||
function receiptFromInstallState(state) {
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
const copyOp = (state.operations || []).find((operation) => operation.moduleId === 'ecc');
|
||||
const mcpAttached = (state.operations || [])
|
||||
.filter((operation) => operation.kind === 'mcp-attach')
|
||||
.map((operation) => operation.moduleId);
|
||||
const hooksOp = (state.operations || []).find((operation) => operation.moduleId === 'hooks');
|
||||
const sourcePin = (state.operations || []).find((operation) => operation.kind === 'source-pin');
|
||||
const enabled = Array.isArray(state.resolution.selectedModules)
|
||||
&& state.resolution.selectedModules.includes(CURRENT_PLUGIN_SLUG);
|
||||
return {
|
||||
id: receiptIdFromState(state),
|
||||
pluginId: CURRENT_PLUGIN_SLUG,
|
||||
version: state.source.repoVersion,
|
||||
source: {
|
||||
source: 'url',
|
||||
url: sourcePin ? sourcePin.destinationPath : '',
|
||||
sha: state.source.repoCommit,
|
||||
},
|
||||
identity: sourcePin
|
||||
? `${sourcePin.destinationPath}@${state.source.repoCommit}`
|
||||
: state.source.repoCommit,
|
||||
installedRoot: copyOp ? copyOp.destinationPath : state.target.root,
|
||||
enabled,
|
||||
trust: (state.operations || []).some((operation) => operation.strategy === 'consent-allow'),
|
||||
capabilities: (state.operations || [])
|
||||
.filter((operation) => operation.moduleId === 'hooks' || operation.kind === 'mcp-attach' || operation.kind === 'mcp-omit')
|
||||
.map((operation) => ({
|
||||
id: operation.moduleId,
|
||||
kind: operation.moduleId === 'hooks' ? 'hooks' : 'mcp',
|
||||
consented: operation.strategy === 'consent-allow',
|
||||
})),
|
||||
hooksEnabled: Boolean(hooksOp && hooksOp.strategy === 'consent-allow'),
|
||||
mcpAttached,
|
||||
previousReceiptId: null,
|
||||
createdAt: state.installedAt,
|
||||
operation: (state.operations || []).some((operation) => operation.kind === 'rollback')
|
||||
? 'rollback'
|
||||
: (state.operations || []).some((operation) => operation.kind === 'uninstall')
|
||||
? 'uninstall'
|
||||
: 'install',
|
||||
installStatePath: state.target.installStatePath,
|
||||
schemaVersion: state.schemaVersion,
|
||||
};
|
||||
}
|
||||
|
||||
function loadCurrentReceipt(homeDir, pathModule = path) {
|
||||
const statePath = grokInstallStatePath(homeDir, pathModule);
|
||||
if (!fs.existsSync(statePath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const state = readInstallState(statePath);
|
||||
const receipt = receiptFromInstallState(state);
|
||||
const previousPath = grokPreviousStatePath(homeDir, pathModule);
|
||||
if (fs.existsSync(previousPath)) {
|
||||
try {
|
||||
const previous = readInstallState(previousPath);
|
||||
receipt.previousReceiptId = receiptIdFromState(previous);
|
||||
} catch {
|
||||
receipt.previousReceiptId = null;
|
||||
}
|
||||
}
|
||||
return receipt;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function installDestination(homeDir, plan, pathModule = path) {
|
||||
const grokRoot = grokHomeDir(homeDir, pathModule);
|
||||
const dest = pathModule.join(
|
||||
grokRoot,
|
||||
INSTALLED_PLUGINS_DIR,
|
||||
`${plan.pluginId || CURRENT_PLUGIN_SLUG}-${String(plan.source.sha).slice(0, 12)}`
|
||||
);
|
||||
assertRootContainment(dest, grokRoot, pathModule);
|
||||
return dest;
|
||||
}
|
||||
|
||||
function readInstalledSharedEnv(installedRoot, pathModule = path) {
|
||||
return readJsonIfPresent(pathModule.join(installedRoot, '.grok-plugin', 'shared-env.json'));
|
||||
}
|
||||
|
||||
function listManagedInstallRoots(homeDir, pathModule = path) {
|
||||
const roots = [];
|
||||
const base = pathModule.join(grokHomeDir(homeDir, pathModule), INSTALLED_PLUGINS_DIR);
|
||||
try {
|
||||
const entries = fs.readdirSync(base, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const installedRoot = pathModule.join(base, entry.name);
|
||||
if (readJsonIfPresent(pathModule.join(installedRoot, SOURCE_IDENTITY_FILE))) {
|
||||
roots.push(installedRoot);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return roots;
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
function planToOperations(plan, dest, pathModule) {
|
||||
const operations = [
|
||||
{
|
||||
kind: 'copy-path',
|
||||
moduleId: CURRENT_PLUGIN_SLUG,
|
||||
sourceRelativePath: '.',
|
||||
destinationPath: dest,
|
||||
strategy: 'preserve-relative-path',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
},
|
||||
{
|
||||
kind: 'source-pin',
|
||||
moduleId: 'marketplace',
|
||||
sourceRelativePath: '.grok-plugin/marketplace.json',
|
||||
destinationPath: plan.source.url,
|
||||
strategy: 'pin-sha',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
},
|
||||
{
|
||||
kind: plan.hooksEnabled ? 'copy-path' : 'omit-path',
|
||||
moduleId: 'hooks',
|
||||
sourceRelativePath: 'hooks/hooks.json',
|
||||
destinationPath: pathModule.join(dest, 'hooks', 'hooks.json'),
|
||||
strategy: plan.hooksEnabled ? 'consent-allow' : 'consent-deny',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
},
|
||||
];
|
||||
for (const capability of plan.capabilities.filter((item) => item.kind === 'mcp')) {
|
||||
operations.push({
|
||||
kind: capability.consented ? 'mcp-attach' : 'mcp-omit',
|
||||
moduleId: capability.id,
|
||||
sourceRelativePath: '.mcp.json',
|
||||
destinationPath: pathModule.join(dest, '.mcp.json'),
|
||||
strategy: capability.consented ? 'consent-allow' : 'consent-deny',
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
});
|
||||
}
|
||||
return operations;
|
||||
}
|
||||
|
||||
function writeGrokInstallState(homeDir, plan, dest, now, pathModule, extraOperations = []) {
|
||||
const installStatePath = grokInstallStatePath(homeDir, pathModule);
|
||||
const operations = planToOperations(plan, dest, pathModule).concat(extraOperations);
|
||||
const state = createInstallState({
|
||||
installedAt: now,
|
||||
adapter: {
|
||||
id: grokHomeTarget.id,
|
||||
target: grokHomeTarget.target,
|
||||
kind: grokHomeTarget.kind,
|
||||
},
|
||||
targetRoot: grokHomeDir(homeDir, pathModule),
|
||||
installStatePath,
|
||||
request: {
|
||||
profile: null,
|
||||
modules: plan.enabled !== false ? [CURRENT_PLUGIN_SLUG] : [],
|
||||
includeComponents: plan.mcpAttached.slice(),
|
||||
excludeComponents: plan.hooksEnabled ? [] : ['hooks'],
|
||||
legacyLanguages: [],
|
||||
legacyMode: false,
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: plan.enabled !== false ? [CURRENT_PLUGIN_SLUG] : [],
|
||||
skippedModules: plan.enabled !== false ? [] : [CURRENT_PLUGIN_SLUG],
|
||||
},
|
||||
source: {
|
||||
repoVersion: plan.version || null,
|
||||
repoCommit: plan.source.sha,
|
||||
manifestVersion: 1,
|
||||
},
|
||||
operations,
|
||||
});
|
||||
writeInstallState(installStatePath, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function resolveGrokPluginRoot(options = {}) {
|
||||
const pathModule = options.pathModule || path;
|
||||
const env = options.env || {};
|
||||
const envRoot = options.envRoot !== undefined
|
||||
? trimEnv(options.envRoot)
|
||||
: grokPluginRootFromEnv(env);
|
||||
if (envRoot) {
|
||||
return envRoot;
|
||||
}
|
||||
|
||||
const homeDir = options.homeDir || os.homedir();
|
||||
const requireEnabled = options.requireEnabled !== false;
|
||||
const current = loadCurrentReceipt(homeDir, pathModule);
|
||||
if (requireEnabled && current && current.enabled === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pinnedSha = options.pinnedSha || (options.source && options.source.sha) || '';
|
||||
if (pinnedSha) {
|
||||
const pinned = selectPinnedCachedVersion(
|
||||
listCachedGrokVersions(homeDir, pathModule),
|
||||
pinnedSha
|
||||
);
|
||||
if (pinned && isCompleteRoot(pinned.installedRoot, options.probe, pathModule)) {
|
||||
return pinned.installedRoot;
|
||||
}
|
||||
}
|
||||
|
||||
if (current && current.installedRoot && isCompleteRoot(current.installedRoot, options.probe, pathModule)) {
|
||||
return current.installedRoot;
|
||||
}
|
||||
|
||||
return findRootInGrokHome(homeDir, options.probe, pathModule);
|
||||
}
|
||||
|
||||
function listCachedGrokVersions(homeDir, pathModule = path) {
|
||||
const versions = [];
|
||||
const cacheBase = pathModule.join(grokHomeDir(homeDir, pathModule), 'plugins', 'cache', CURRENT_PLUGIN_SLUG);
|
||||
try {
|
||||
const orgDirs = fs.readdirSync(cacheBase, { withFileTypes: true });
|
||||
for (const orgEntry of orgDirs) {
|
||||
for (const orgEntry of fs.readdirSync(cacheBase, { withFileTypes: true })) {
|
||||
if (!orgEntry.isDirectory()) continue;
|
||||
const orgPath = pathModule.join(cacheBase, orgEntry.name);
|
||||
const versionDirs = fs.readdirSync(orgPath, { withFileTypes: true });
|
||||
for (const verEntry of versionDirs) {
|
||||
if (!verEntry.isDirectory()) continue;
|
||||
const installedRoot = pathModule.join(orgPath, verEntry.name);
|
||||
for (const versionEntry of fs.readdirSync(orgPath, { withFileTypes: true })) {
|
||||
if (!versionEntry.isDirectory()) continue;
|
||||
const installedRoot = pathModule.join(orgPath, versionEntry.name);
|
||||
const identity = readJsonIfPresent(pathModule.join(installedRoot, SOURCE_IDENTITY_FILE));
|
||||
versions.push({
|
||||
org: orgEntry.name,
|
||||
version: verEntry.name,
|
||||
version: versionEntry.name,
|
||||
installedRoot,
|
||||
sha: identity && identity.sha,
|
||||
});
|
||||
@@ -531,331 +90,64 @@ function listCachedGrokVersions(homeDir, pathModule = path) {
|
||||
}
|
||||
|
||||
function selectPinnedCachedVersion(versions, sha) {
|
||||
if (!SHA_PATTERN.test(sha || '')) {
|
||||
return null;
|
||||
}
|
||||
if (!SHA_PATTERN.test(sha || '')) return null;
|
||||
return versions.find((entry) => entry.sha === sha) || null;
|
||||
}
|
||||
|
||||
function capabilityConsented(consent, capability) {
|
||||
if (!consent || typeof consent !== 'object') {
|
||||
return false;
|
||||
function findRootInGrokHome(homeDir, probe, pathModule = path) {
|
||||
const grokRoot = grokHomeDir(homeDir, pathModule);
|
||||
const candidates = [
|
||||
pathModule.join(grokRoot, 'plugins', CURRENT_PLUGIN_SLUG),
|
||||
pathModule.join(grokRoot, 'plugins', `${CURRENT_PLUGIN_SLUG}@${CURRENT_PLUGIN_SLUG}`),
|
||||
pathModule.join(grokRoot, 'plugins', 'marketplaces', CURRENT_PLUGIN_SLUG),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (isCompleteRoot(candidate, probe, pathModule)) return candidate;
|
||||
}
|
||||
if (capability.kind === 'hooks') {
|
||||
return consent.hooks === true;
|
||||
}
|
||||
if (capability.kind === 'mcp') {
|
||||
const mcp = consent.mcp && typeof consent.mcp === 'object' ? consent.mcp : {};
|
||||
return mcp[capability.id] === true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function previewInstall(options = {}) {
|
||||
const pathModule = options.pathModule || path;
|
||||
const source = assertPinnedSource(options.source);
|
||||
const trust = options.trust === true;
|
||||
const consent = options.consent && typeof options.consent === 'object' ? options.consent : {};
|
||||
const mcpCapabilities = listMcpCapabilities(options.mcpConfig || {});
|
||||
const capabilities = [
|
||||
{ id: HOOKS_CAPABILITY_ID, kind: 'hooks' },
|
||||
...mcpCapabilities,
|
||||
].map((capability) => {
|
||||
const consented = trust && capabilityConsented(consent, capability);
|
||||
return {
|
||||
...capability,
|
||||
consented,
|
||||
};
|
||||
});
|
||||
|
||||
const mcpAttached = capabilities
|
||||
.filter((capability) => capability.kind === 'mcp' && capability.consented)
|
||||
.map((capability) => capability.id);
|
||||
const hooksEnabled = capabilities.some(
|
||||
(capability) => capability.id === HOOKS_CAPABILITY_ID && capability.consented
|
||||
);
|
||||
|
||||
return {
|
||||
kind: 'grok-install-plan',
|
||||
pluginId: options.pluginId || CURRENT_PLUGIN_SLUG,
|
||||
version: options.version || null,
|
||||
trust,
|
||||
enabled: options.enabled !== false,
|
||||
source,
|
||||
identity: sourceIdentity(source),
|
||||
capabilities,
|
||||
hooksEnabled,
|
||||
mcpAttached,
|
||||
attachRootMcp: mcpAttached.length > 0,
|
||||
attachChromeDevtools: mcpAttached.includes(CHROME_DEVTOOLS_MCP),
|
||||
homeDir: options.homeDir,
|
||||
sourceRoot: options.sourceRoot || null,
|
||||
pathModule: pathModule === path.win32 ? 'win32' : 'posix',
|
||||
};
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function filterMcpConfig(mcpConfig, attachedNames) {
|
||||
const servers = mcpConfig && mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object'
|
||||
? mcpConfig.mcpServers
|
||||
: {};
|
||||
const mcpServers = {};
|
||||
for (const name of attachedNames) {
|
||||
if (servers[name]) {
|
||||
mcpServers[name] = servers[name];
|
||||
for (const dirname of ['installed-plugins', 'plugins']) {
|
||||
const base = pathModule.join(grokRoot, dirname);
|
||||
try {
|
||||
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const candidate = pathModule.join(base, entry.name);
|
||||
if (isCompleteRoot(candidate, probe, pathModule)) return candidate;
|
||||
}
|
||||
} catch {
|
||||
// Optional native discovery root.
|
||||
}
|
||||
}
|
||||
return { mcpServers };
|
||||
return null;
|
||||
}
|
||||
|
||||
const SHARED_INLINE_ENV = 'process.env.PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT';
|
||||
const INSTALLED_INLINE_ENV = `${SHARED_INLINE_ENV}||process.env.GROK_PLUGIN_ROOT`;
|
||||
|
||||
function applyInstalledHookBoundary(dest, pathModule) {
|
||||
const mapped = toSharedPluginEnv({ GROK_PLUGIN_ROOT: dest });
|
||||
writeJson(pathModule.join(dest, '.grok-plugin', 'shared-env.json'), {
|
||||
PLUGIN_ROOT: mapped.PLUGIN_ROOT,
|
||||
});
|
||||
const destHooks = pathModule.join(dest, 'hooks', 'hooks.json');
|
||||
if (!fs.existsSync(destHooks)) {
|
||||
return;
|
||||
}
|
||||
const text = fs.readFileSync(destHooks, 'utf8');
|
||||
fs.writeFileSync(destHooks, text.split(SHARED_INLINE_ENV).join(INSTALLED_INLINE_ENV));
|
||||
}
|
||||
|
||||
function applyInstall(plan, options = {}) {
|
||||
if (!plan || plan.kind !== 'grok-install-plan') {
|
||||
throw new Error('applyInstall requires a grok-install-plan');
|
||||
}
|
||||
function resolveGrokPluginRoot(options = {}) {
|
||||
const pathModule = options.pathModule || path;
|
||||
const homeDir = plan.homeDir || options.homeDir;
|
||||
if (!homeDir) {
|
||||
throw new Error('homeDir is required to apply a Grok install');
|
||||
const envRoot = options.envRoot !== undefined
|
||||
? trimEnv(options.envRoot)
|
||||
: grokPluginRootFromEnv(options.env || {});
|
||||
if (envRoot) return envRoot;
|
||||
if (options.enabled === false) return null;
|
||||
|
||||
const homeDir = options.homeDir || os.homedir();
|
||||
const pinnedSha = options.pinnedSha || (options.source && options.source.sha) || '';
|
||||
if (pinnedSha) {
|
||||
const pinned = selectPinnedCachedVersion(listCachedGrokVersions(homeDir, pathModule), pinnedSha);
|
||||
if (pinned && isCompleteRoot(pinned.installedRoot, options.probe, pathModule)) {
|
||||
return pinned.installedRoot;
|
||||
}
|
||||
}
|
||||
const dest = installDestination(homeDir, plan, pathModule);
|
||||
|
||||
if (!plan.sourceRoot) {
|
||||
throw new Error('sourceRoot is required to apply a Grok install');
|
||||
}
|
||||
assertSourceIdentity(plan.sourceRoot, plan.source, pathModule);
|
||||
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
copyInstallSource(plan.sourceRoot, dest, pathModule, plan.source.sha);
|
||||
writeJson(pathModule.join(dest, SOURCE_IDENTITY_FILE), plan.source);
|
||||
writePinnedMarketplace(dest, plan.source, pathModule);
|
||||
writeJson(
|
||||
pathModule.join(dest, '.mcp.json'),
|
||||
filterMcpConfig(options.mcpConfig || plan.mcpConfig, plan.mcpAttached)
|
||||
);
|
||||
|
||||
if (!plan.hooksEnabled) {
|
||||
fs.rmSync(pathModule.join(dest, 'hooks'), { recursive: true, force: true });
|
||||
} else {
|
||||
applyInstalledHookBoundary(dest, pathModule);
|
||||
}
|
||||
|
||||
const now = options.now || new Date().toISOString();
|
||||
const statePath = grokInstallStatePath(homeDir, pathModule);
|
||||
const previousPath = grokPreviousStatePath(homeDir, pathModule);
|
||||
if (fs.existsSync(statePath)) {
|
||||
fs.mkdirSync(pathModule.dirname(previousPath), { recursive: true });
|
||||
fs.copyFileSync(statePath, previousPath);
|
||||
}
|
||||
|
||||
writeGrokInstallState(homeDir, plan, dest, now, pathModule);
|
||||
const receipt = loadCurrentReceipt(homeDir, pathModule);
|
||||
receipt.operation = fs.existsSync(previousPath) ? 'upgrade' : 'install';
|
||||
receipt.trust = plan.trust === true;
|
||||
return receipt;
|
||||
}
|
||||
|
||||
function uninstall(homeDir, options = {}) {
|
||||
const pathModule = options.pathModule || path;
|
||||
const current = loadCurrentReceipt(homeDir, pathModule);
|
||||
if (!current) {
|
||||
return { operation: 'uninstall', removed: false };
|
||||
}
|
||||
const managedRoots = listManagedInstallRoots(homeDir, pathModule);
|
||||
for (const installedRoot of managedRoots) {
|
||||
fs.rmSync(installedRoot, { recursive: true, force: true });
|
||||
}
|
||||
const statePath = grokInstallStatePath(homeDir, pathModule);
|
||||
const previousPath = grokPreviousStatePath(homeDir, pathModule);
|
||||
fs.rmSync(statePath, { force: true });
|
||||
fs.rmSync(previousPath, { force: true });
|
||||
return {
|
||||
...current,
|
||||
enabled: false,
|
||||
operation: 'uninstall',
|
||||
removedRoots: managedRoots,
|
||||
uninstalledAt: options.now || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function rollback(homeDir, options = {}) {
|
||||
const pathModule = options.pathModule || path;
|
||||
const current = loadCurrentReceipt(homeDir, pathModule);
|
||||
const previousPath = grokPreviousStatePath(homeDir, pathModule);
|
||||
if (!current || !fs.existsSync(previousPath)) {
|
||||
throw new Error('no previous receipt to roll back to');
|
||||
}
|
||||
const previousState = readInstallState(previousPath);
|
||||
const previousReceipt = receiptFromInstallState(previousState);
|
||||
if (!previousReceipt.installedRoot || !fs.existsSync(previousReceipt.installedRoot)) {
|
||||
throw new Error('previous install is not available for rollback');
|
||||
}
|
||||
const statePath = grokInstallStatePath(homeDir, pathModule);
|
||||
fs.copyFileSync(previousPath, statePath);
|
||||
fs.rmSync(previousPath, { force: true });
|
||||
if (current.installedRoot && current.installedRoot !== previousReceipt.installedRoot) {
|
||||
fs.rmSync(current.installedRoot, { recursive: true, force: true });
|
||||
}
|
||||
return {
|
||||
...loadCurrentReceipt(homeDir, pathModule),
|
||||
operation: 'rollback',
|
||||
rolledBackFrom: current.id,
|
||||
};
|
||||
}
|
||||
|
||||
function setPluginEnabled(homeDir, enabled, options = {}) {
|
||||
const pathModule = options.pathModule || path;
|
||||
const statePath = grokInstallStatePath(homeDir, pathModule);
|
||||
if (!fs.existsSync(statePath)) {
|
||||
throw new Error('no current Grok install to enable or disable');
|
||||
}
|
||||
const state = readInstallState(statePath);
|
||||
const next = createInstallState({
|
||||
installedAt: state.installedAt,
|
||||
adapter: {
|
||||
id: state.target.id,
|
||||
target: state.target.target,
|
||||
kind: state.target.kind,
|
||||
},
|
||||
targetRoot: state.target.root,
|
||||
installStatePath: state.target.installStatePath,
|
||||
request: {
|
||||
...state.request,
|
||||
modules: enabled ? [CURRENT_PLUGIN_SLUG] : [],
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: enabled ? [CURRENT_PLUGIN_SLUG] : [],
|
||||
skippedModules: enabled ? [] : [CURRENT_PLUGIN_SLUG],
|
||||
},
|
||||
source: state.source,
|
||||
operations: state.operations,
|
||||
});
|
||||
writeInstallState(statePath, next);
|
||||
return loadCurrentReceipt(homeDir, pathModule);
|
||||
}
|
||||
|
||||
function parseMcpConsentList(raw) {
|
||||
const mcp = {};
|
||||
String(raw || '')
|
||||
.split(',')
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((name) => {
|
||||
mcp[name] = true;
|
||||
});
|
||||
return mcp;
|
||||
}
|
||||
|
||||
function resolveRepoRoot(candidate) {
|
||||
const start = candidate || path.join(__dirname, '..', '..');
|
||||
try {
|
||||
const { execFileSync } = require('child_process');
|
||||
return execFileSync('git', ['-C', start, 'rev-parse', '--show-toplevel'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
} catch {
|
||||
return start;
|
||||
}
|
||||
}
|
||||
|
||||
function runGrokInstall(options = {}) {
|
||||
const repoRoot = resolveRepoRoot(options.repoRoot || path.join(__dirname, '..', '..'));
|
||||
const homeDir = options.homeDir || resolveHomeDir();
|
||||
const marketplace = readMarketplaceSource(path.join(repoRoot, '.grok-plugin', 'marketplace.json'));
|
||||
const pluginManifest = readGrokPluginManifest(path.join(repoRoot, '.grok-plugin', 'plugin.json'));
|
||||
let mcpConfig = { mcpServers: {} };
|
||||
try {
|
||||
mcpConfig = JSON.parse(fs.readFileSync(path.join(repoRoot, '.mcp.json'), 'utf8'));
|
||||
} catch {
|
||||
mcpConfig = { mcpServers: {} };
|
||||
}
|
||||
|
||||
const plan = previewInstall({
|
||||
source: marketplace.source,
|
||||
version: marketplace.version,
|
||||
sourceRoot: repoRoot,
|
||||
homeDir,
|
||||
trust: options.trust === true,
|
||||
consent: options.consent && typeof options.consent === 'object' ? options.consent : {},
|
||||
mcpConfig,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const result = {
|
||||
kind: 'grok-install-result',
|
||||
dryRun: options.dryRun === true,
|
||||
plan,
|
||||
nativeMcpOptedOut: nativePluginMcpOptedOut(pluginManifest),
|
||||
nativeHooksOptedOut: pluginManifest.hooks === '',
|
||||
};
|
||||
|
||||
if (options.dryRun) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.receipt = applyInstall(plan, { mcpConfig, homeDir });
|
||||
return result;
|
||||
return findRootInGrokHome(homeDir, options.probe, pathModule);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GROK_PLUGIN_ROOT_ENV,
|
||||
GROK_HOME_DIRNAME,
|
||||
SOURCE_IDENTITY_FILE,
|
||||
SHA_PATTERN,
|
||||
HOOKS_CAPABILITY_ID,
|
||||
CHROME_DEVTOOLS_MCP,
|
||||
grokPluginRootFromEnv,
|
||||
toSharedPluginEnv,
|
||||
nativePluginMcpOptedOut,
|
||||
nativeTrustedMcpServerNames,
|
||||
nativeTrustedInstallAttachesChromeDevtools,
|
||||
readGrokPluginManifest,
|
||||
resolveHomeDir,
|
||||
grokHomeDir,
|
||||
grokInstallStatePath,
|
||||
isContained,
|
||||
assertRootContainment,
|
||||
assertPinnedSource,
|
||||
sourceIdentity,
|
||||
readMarketplaceSource,
|
||||
listMcpCapabilities,
|
||||
resolveGrokPluginRoot,
|
||||
listCachedGrokVersions,
|
||||
selectPinnedCachedVersion,
|
||||
installDestination,
|
||||
readInstalledSharedEnv,
|
||||
listManagedInstallRoots,
|
||||
planToOperations,
|
||||
previewInstall,
|
||||
applyInstall,
|
||||
uninstall,
|
||||
rollback,
|
||||
setPluginEnabled,
|
||||
loadCurrentReceipt,
|
||||
parseMcpConsentList,
|
||||
resolveRepoRoot,
|
||||
runGrokInstall,
|
||||
isGitWorkTreeRoot,
|
||||
resolvePinnedGitSha,
|
||||
assertSourceIdentity,
|
||||
};
|
||||
@@ -2,9 +2,17 @@
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
||||
const TEMP_SOURCE_ROOTS = new Set();
|
||||
|
||||
process.once('exit', () => {
|
||||
for (const tempRoot of TEMP_SOURCE_ROOTS) {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function isGitWorkTreeRoot(dir, pathModule = path) {
|
||||
try {
|
||||
@@ -74,65 +82,163 @@ function copyGitArchive(sourceRoot, sha, dest, pathModule = path) {
|
||||
}
|
||||
}
|
||||
|
||||
function copyInstallSource(sourceRoot, dest, pathModule = path, sha) {
|
||||
if (isGitWorkTreeRoot(sourceRoot, pathModule)) {
|
||||
if (!resolvePinnedGitSha(sourceRoot, sha)) {
|
||||
throw new Error(`pinned SHA ${sha} is not in source git`);
|
||||
}
|
||||
copyGitArchive(sourceRoot, sha, dest, pathModule);
|
||||
return { mode: 'git-archive', sha };
|
||||
}
|
||||
|
||||
fs.cpSync(sourceRoot, dest, { recursive: true });
|
||||
return { mode: 'tree' };
|
||||
}
|
||||
|
||||
function assertSourceIdentity(sourceRoot, source, pathModule = path) {
|
||||
if (isGitWorkTreeRoot(sourceRoot, pathModule)) {
|
||||
if (!resolvePinnedGitSha(sourceRoot, source && source.sha)) {
|
||||
throw new Error(`pinned SHA ${source && source.sha} is not in source git`);
|
||||
}
|
||||
return 'git-archive';
|
||||
}
|
||||
|
||||
function readPinnedMarketplaceSource(sourceRoot, pathModule = path) {
|
||||
const catalogPath = pathModule.join(sourceRoot, '.grok-plugin', 'marketplace.json');
|
||||
if (!fs.existsSync(catalogPath)) {
|
||||
return 'tree';
|
||||
let catalog;
|
||||
try {
|
||||
catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read Grok marketplace source at ${catalogPath}: ${error.message}`);
|
||||
}
|
||||
|
||||
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||||
const plugin = catalog && Array.isArray(catalog.plugins) ? catalog.plugins[0] : null;
|
||||
const catalogSha = plugin && plugin.source && plugin.source.sha;
|
||||
if (catalogSha && catalogSha !== source.sha) {
|
||||
throw new Error('sourceRoot marketplace sha does not match install plan pin');
|
||||
const source = plugin && plugin.source;
|
||||
if (!source || source.source !== 'url' || typeof source.url !== 'string' || !source.url.trim()) {
|
||||
throw new Error('Grok marketplace source must be a Git URL');
|
||||
}
|
||||
return 'tree';
|
||||
}
|
||||
|
||||
function writePinnedMarketplace(dest, source, pathModule = path) {
|
||||
const catalogPath = pathModule.join(dest, '.grok-plugin', 'marketplace.json');
|
||||
if (!fs.existsSync(catalogPath)) {
|
||||
return false;
|
||||
if (!SHA_PATTERN.test(source.sha || '')) {
|
||||
throw new Error('Grok marketplace source must pin a 40-character lowercase commit SHA');
|
||||
}
|
||||
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||||
if (!catalog.plugins || !catalog.plugins[0]) {
|
||||
return false;
|
||||
if (typeof plugin.version !== 'string' || !plugin.version.trim()) {
|
||||
throw new Error('Grok marketplace source must declare a plugin version');
|
||||
}
|
||||
catalog.plugins[0].source = {
|
||||
source: source.source,
|
||||
return {
|
||||
url: source.url,
|
||||
sha: source.sha,
|
||||
version: plugin.version,
|
||||
};
|
||||
fs.mkdirSync(pathModule.dirname(catalogPath), { recursive: true });
|
||||
fs.writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeGitLocation(value, pathModule = path) {
|
||||
const location = String(value || '').trim().replace(/\/+$/, '').replace(/\.git$/, '');
|
||||
if (/^(?:[a-z][a-z0-9+.-]*:\/\/|git@)/i.test(location)) {
|
||||
return location;
|
||||
}
|
||||
return pathModule.resolve(location);
|
||||
}
|
||||
|
||||
function findRegistrySource(sourceRoot, homeDir, pathModule = path) {
|
||||
const registryPath = pathModule.join(homeDir, '.grok', 'installed-plugins', 'registry.json');
|
||||
let registry;
|
||||
try {
|
||||
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw new Error(`Failed to read Grok plugin registry at ${registryPath}: ${error.message}`);
|
||||
}
|
||||
const expectedPath = fs.realpathSync(sourceRoot);
|
||||
for (const record of Object.values(registry.repos || {})) {
|
||||
let recordPath;
|
||||
try {
|
||||
recordPath = fs.realpathSync(record.path);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (recordPath !== expectedPath || String(record.kind && record.kind.type).toLowerCase() !== 'git') {
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
url: record.kind.url,
|
||||
sha: record.kind.commit,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function fetchPinnedGitSource(sourceUrl, sha, parentDir) {
|
||||
const gitDir = path.join(parentDir, 'git');
|
||||
fs.mkdirSync(gitDir, { recursive: true });
|
||||
execFileSync('git', ['init', '--bare', '--quiet', gitDir], { stdio: 'ignore' });
|
||||
execFileSync('git', ['-C', gitDir, 'fetch', '--quiet', '--depth=1', '--no-tags', sourceUrl, sha], {
|
||||
stdio: ['ignore', 'ignore', 'pipe'],
|
||||
});
|
||||
const fetched = execFileSync('git', ['-C', gitDir, 'rev-parse', 'FETCH_HEAD^{commit}'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
if (fetched !== sha) {
|
||||
throw new Error(`Fetched Grok source commit ${fetched} does not match pinned SHA ${sha}`);
|
||||
}
|
||||
return gitDir;
|
||||
}
|
||||
|
||||
function assertNoSymlinks(root) {
|
||||
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
||||
const entryPath = path.join(root, entry.name);
|
||||
if (entry.isSymbolicLink()) {
|
||||
throw new Error(`Pinned Grok source contains unsupported symlink: ${entryPath}`);
|
||||
}
|
||||
if (entry.isDirectory()) assertNoSymlinks(entryPath);
|
||||
}
|
||||
}
|
||||
|
||||
function preparePinnedGrokSource(options = {}) {
|
||||
const sourceRoot = path.resolve(options.sourceRoot || '');
|
||||
const homeDir = path.resolve(options.homeDir || os.homedir());
|
||||
const catalogSource = readPinnedMarketplaceSource(sourceRoot);
|
||||
const sourceUrl = options.sourceUrl || catalogSource.url;
|
||||
const sourceSha = options.sourceSha || catalogSource.sha;
|
||||
if (normalizeGitLocation(sourceUrl) !== normalizeGitLocation(catalogSource.url)) {
|
||||
throw new Error('Recorded Grok source URL does not match the marketplace source');
|
||||
}
|
||||
if (!SHA_PATTERN.test(sourceSha || '')) {
|
||||
throw new Error('Recorded Grok source must pin a 40-character lowercase commit SHA');
|
||||
}
|
||||
|
||||
let gitSourceRoot = null;
|
||||
if (isGitWorkTreeRoot(sourceRoot) && resolvePinnedGitSha(sourceRoot, sourceSha)) {
|
||||
gitSourceRoot = sourceRoot;
|
||||
} else {
|
||||
const registrySource = findRegistrySource(sourceRoot, homeDir);
|
||||
if (!registrySource
|
||||
|| normalizeGitLocation(registrySource.url) !== normalizeGitLocation(sourceUrl)
|
||||
|| registrySource.sha !== sourceSha) {
|
||||
throw new Error('Non-Git Grok source is unverifiable without matching registry URL/SHA evidence');
|
||||
}
|
||||
}
|
||||
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-grok-source-'));
|
||||
TEMP_SOURCE_ROOTS.add(tempRoot);
|
||||
const snapshotRoot = path.join(tempRoot, 'snapshot');
|
||||
try {
|
||||
if (!gitSourceRoot) {
|
||||
gitSourceRoot = fetchPinnedGitSource(sourceUrl, sourceSha, tempRoot);
|
||||
}
|
||||
copyGitArchive(gitSourceRoot, sourceSha, snapshotRoot);
|
||||
assertNoSymlinks(snapshotRoot);
|
||||
const snapshotManifest = JSON.parse(
|
||||
fs.readFileSync(path.join(snapshotRoot, '.grok-plugin', 'plugin.json'), 'utf8')
|
||||
);
|
||||
if (snapshotManifest.version !== catalogSource.version) {
|
||||
throw new Error(
|
||||
`Pinned Grok plugin version ${snapshotManifest.version || '(missing)'} does not match marketplace version ${catalogSource.version}`
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(path.join(snapshotRoot, '.ecc-source.json'), `${JSON.stringify({
|
||||
source: 'url',
|
||||
url: sourceUrl,
|
||||
sha: sourceSha,
|
||||
}, null, 2)}\n`);
|
||||
return {
|
||||
sourceRoot: snapshotRoot,
|
||||
sourceUrl,
|
||||
sourceSha,
|
||||
sourceVersion: catalogSource.version,
|
||||
cleanup: () => {
|
||||
TEMP_SOURCE_ROOTS.delete(tempRoot);
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
TEMP_SOURCE_ROOTS.delete(tempRoot);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SHA_PATTERN,
|
||||
isGitWorkTreeRoot,
|
||||
resolvePinnedGitSha,
|
||||
copyInstallSource,
|
||||
assertSourceIdentity,
|
||||
writePinnedMarketplace,
|
||||
readPinnedMarketplaceSource,
|
||||
preparePinnedGrokSource,
|
||||
};
|
||||
@@ -114,7 +114,7 @@ const ADAPTER_RECORDS = Object.freeze([
|
||||
'No plugin rules/ surface',
|
||||
],
|
||||
install_or_onramp: [
|
||||
'`node scripts/grok-install.js --dry-run` / `node scripts/grok-install.js` (ECC 2 `grok` install-state under ~/.grok/ecc/)',
|
||||
'`node scripts/grok-install.js --dry-run` / `node scripts/grok-install.js` (canonical `grok` install-state under ~/.grok/ecc/)',
|
||||
'`grok plugin marketplace add affaan-m/ECC && grok plugin install ecc && grok plugin enable ecc` (discovery only; not ECC consent)',
|
||||
],
|
||||
verification_commands: [
|
||||
@@ -124,7 +124,7 @@ const ADAPTER_RECORDS = Object.freeze([
|
||||
],
|
||||
risk_notes: [
|
||||
'Grok rejects Claude marketplace source "./"; pin a url+sha catalog in .grok-plugin/marketplace.json to a commit that contains the Grok adapter',
|
||||
'ECC consent is grok-install.js / applyInstall; native Grok CLI discovery opts hooks and MCP out via empty plugin.json fields; the full skill catalog is large',
|
||||
'ECC consent is the canonical install-apply receipt path; native Grok CLI discovery opts hooks and MCP out via empty plugin.json fields; the full skill catalog is large',
|
||||
],
|
||||
last_verified_at: '2026-09-03',
|
||||
owner: 'ECC maintainers',
|
||||
|
||||
@@ -265,12 +265,12 @@ const HARNESS_CAPABILITIES = deepFreeze([
|
||||
installMode: 'native-plugin',
|
||||
guidedReady: false,
|
||||
availability: 'advanced',
|
||||
destination: '~/.grok through the ECC Grok install-state adapter',
|
||||
destination: '~/.grok',
|
||||
scopes: [scope('native', 'grok', '~/.grok')],
|
||||
hooks: hooks(
|
||||
'native-trust',
|
||||
true,
|
||||
'ECC Grok installs use previewInstall/applyInstall with per-capability consent. Native Grok CLI --trust opts MCP out via empty mcpServers in .grok-plugin/plugin.json.'
|
||||
'ECC Grok installs use the canonical plan/apply receipt with per-capability consent. Native Grok CLI --trust opts MCP out via empty mcpServers in .grok-plugin/plugin.json.'
|
||||
),
|
||||
aliases: ['grok-build'],
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ function sanitizeDiagnostic(value) {
|
||||
|
||||
function readManagedHookConfig(env = process.env) {
|
||||
const pluginRoot = String(
|
||||
env.GROK_PLUGIN_ROOT || env.CLAUDE_PLUGIN_ROOT || env.ECC_PLUGIN_ROOT || ''
|
||||
env.CLAUDE_PLUGIN_ROOT || env.ECC_PLUGIN_ROOT || ''
|
||||
).trim();
|
||||
const configPath = String(env.ECC_HOOK_CONFIG || '').trim()
|
||||
|| (pluginRoot ? path.join(pluginRoot, 'ecc', 'setup.json') : '');
|
||||
|
||||
@@ -552,7 +552,9 @@ function createLegacyCompatInstallPlan(options = {}) {
|
||||
requestModuleIds: [],
|
||||
requestIncludeComponentIds: includeComponentIds,
|
||||
requestExcludeComponentIds: excludeComponentIds,
|
||||
mode: 'legacy-compat'
|
||||
mode: 'legacy-compat',
|
||||
trust: options.trust === true,
|
||||
consent: options.consent || {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const { getInstallTargetAdapter, planInstallTargetScaffold } = require('./instal
|
||||
const { resolveInvocationEnvironment } = require('./invocation-environment');
|
||||
|
||||
const DEFAULT_REPO_ROOT = path.join(__dirname, '../..');
|
||||
const SUPPORTED_INSTALL_TARGETS = ['claude', 'claude-project', 'cursor', 'antigravity', 'codex', 'copilot', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen', 'zed', 'hermes', 'openclaw', 'kimi', 'adal'];
|
||||
const SUPPORTED_INSTALL_TARGETS = ['claude', 'claude-project', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen', 'zed', 'hermes', 'openclaw', 'kimi', 'adal', 'grok'];
|
||||
const COMPONENT_FAMILY_PREFIXES = {
|
||||
baseline: 'baseline:',
|
||||
language: 'lang:',
|
||||
@@ -618,6 +618,9 @@ function resolveInstallPlan(options = {}) {
|
||||
projectRoot: validatedProjectRoot || manifests.repoRoot,
|
||||
homeDir: validatedHomeDir || os.homedir(),
|
||||
env: resolveInvocationEnvironment(options),
|
||||
trust: options.trust === true,
|
||||
consent: options.consent || {},
|
||||
sourceSha: options.sourceSha || null,
|
||||
}
|
||||
: null;
|
||||
const targetAdapter = target ? getInstallTargetAdapter(target) : null;
|
||||
@@ -661,7 +664,8 @@ function resolveInstallPlan(options = {}) {
|
||||
|
||||
const supportsTarget = !target
|
||||
|| (
|
||||
readModuleTargetsOrThrow(module).includes(target)
|
||||
(readModuleTargetsOrThrow(module).includes(target)
|
||||
|| targetAdapter?.acceptsUndeclaredModuleTargets === true)
|
||||
&& (!targetAdapter || targetAdapter.supportsModule(module, targetPlanningInput))
|
||||
);
|
||||
|
||||
@@ -730,6 +734,9 @@ function resolveInstallPlan(options = {}) {
|
||||
projectRoot: targetPlanningInput.projectRoot,
|
||||
homeDir: targetPlanningInput.homeDir,
|
||||
env: targetPlanningInput.env,
|
||||
trust: targetPlanningInput.trust,
|
||||
consent: targetPlanningInput.consent,
|
||||
sourceSha: targetPlanningInput.sourceSha,
|
||||
modules: selectedModules,
|
||||
exemptValidationCodes: options.exemptValidationCodes || [],
|
||||
})
|
||||
|
||||
@@ -132,7 +132,7 @@ function createFallbackValidator() {
|
||||
validateNoAdditionalProperties(
|
||||
request,
|
||||
'/request',
|
||||
['profile', 'modules', 'includeComponents', 'excludeComponents', 'legacyLanguages', 'legacyMode', 'hookConsent']
|
||||
['profile', 'modules', 'includeComponents', 'excludeComponents', 'legacyLanguages', 'legacyMode', 'hookConsent', 'trust', 'consentMcp']
|
||||
);
|
||||
if (!(Object.prototype.hasOwnProperty.call(request, 'profile') && (request.profile === null || typeof request.profile === 'string'))) {
|
||||
pushError('/request/profile', 'must be string or null');
|
||||
@@ -152,6 +152,12 @@ function createFallbackValidator() {
|
||||
) {
|
||||
pushError('/request/hookConsent', 'must be enabled, declined, or null');
|
||||
}
|
||||
if (request.trust !== undefined && typeof request.trust !== 'boolean') {
|
||||
pushError('/request/trust', 'must be boolean');
|
||||
}
|
||||
if (request.consentMcp !== undefined) {
|
||||
validateStringArray(request.consentMcp, '/request/consentMcp');
|
||||
}
|
||||
}
|
||||
|
||||
const resolution = state.resolution;
|
||||
@@ -167,8 +173,9 @@ function createFallbackValidator() {
|
||||
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
||||
pushError('/source', 'must be object');
|
||||
} else {
|
||||
validateNoAdditionalProperties(source, '/source', ['repoVersion', 'repoCommit', 'manifestVersion']);
|
||||
validateNoAdditionalProperties(source, '/source', ['repoVersion', 'repoUrl', 'repoCommit', 'manifestVersion']);
|
||||
validateOptionalString(source.repoVersion, '/source/repoVersion');
|
||||
validateOptionalString(source.repoUrl, '/source/repoUrl');
|
||||
validateOptionalString(source.repoCommit, '/source/repoCommit');
|
||||
if (!Number.isInteger(source.manifestVersion) || source.manifestVersion < 1) {
|
||||
pushError('/source/manifestVersion', 'must be integer >= 1');
|
||||
@@ -306,6 +313,10 @@ function createInstallState(options) {
|
||||
hookConsent: Object.prototype.hasOwnProperty.call(options.request, 'hookConsent')
|
||||
? options.request.hookConsent
|
||||
: null,
|
||||
trust: options.request.trust === true,
|
||||
consentMcp: Array.isArray(options.request.consentMcp)
|
||||
? [...options.request.consentMcp]
|
||||
: [],
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: Array.isArray(options.resolution.selectedModules)
|
||||
@@ -317,6 +328,7 @@ function createInstallState(options) {
|
||||
},
|
||||
source: {
|
||||
repoVersion: options.source.repoVersion || null,
|
||||
repoUrl: options.source.repoUrl || null,
|
||||
repoCommit: options.source.repoCommit || null,
|
||||
manifestVersion: options.source.manifestVersion,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { preparePinnedGrokSource } = require('../grok-source-identity');
|
||||
|
||||
const {
|
||||
createInstallTargetAdapter,
|
||||
isForeignPlatformPath,
|
||||
@@ -19,31 +23,104 @@ function isHooksPath(relativePath) {
|
||||
return normalized === 'hooks' || normalized.startsWith('hooks/');
|
||||
}
|
||||
|
||||
function readMcpConfig(repoRoot) {
|
||||
const configPath = path.join(repoRoot, '.mcp.json');
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return { mcpServers: {} };
|
||||
throw new Error(`Failed to parse Grok MCP config at ${configPath}: ${error.message}`);
|
||||
}
|
||||
if (!config || typeof config !== 'object' || Array.isArray(config)
|
||||
|| !config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
|
||||
throw new Error(`Invalid Grok MCP config at ${configPath}: expected an mcpServers object`);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
module.exports = createInstallTargetAdapter({
|
||||
id: 'grok-home',
|
||||
target: 'grok',
|
||||
kind: 'home',
|
||||
acceptsUndeclaredModuleTargets: true,
|
||||
rootSegments: ['.grok'],
|
||||
installStatePathSegments: ['ecc', 'install-state.json'],
|
||||
nativeRootRelativePath: '.grok',
|
||||
prepareSource(input = {}) {
|
||||
return preparePinnedGrokSource({
|
||||
sourceRoot: input.sourceRoot || input.repoRoot,
|
||||
homeDir: input.homeDir,
|
||||
sourceUrl: input.sourceUrl,
|
||||
sourceSha: input.sourceSha,
|
||||
});
|
||||
},
|
||||
planOperations(input = {}, adapter) {
|
||||
const trust = input.trust === true;
|
||||
const consent = input.consent && typeof input.consent === 'object' ? input.consent : {};
|
||||
const allowHooks = trust && consent.hooks === true;
|
||||
const requestedMcpIds = Object.entries(consent.mcp || {})
|
||||
.filter(([, allowed]) => allowed === true)
|
||||
.map(([id]) => id);
|
||||
const mcpConfig = readMcpConfig(input.repoRoot);
|
||||
const unknownMcpIds = requestedMcpIds.filter((id) => !Object.hasOwn(mcpConfig.mcpServers, id));
|
||||
if (unknownMcpIds.length > 0) {
|
||||
throw new Error(`Unknown Grok MCP capability: ${unknownMcpIds.join(', ')}`);
|
||||
}
|
||||
const allowedMcpIds = trust ? requestedMcpIds : [];
|
||||
const modules = Array.isArray(input.modules) ? input.modules : [];
|
||||
|
||||
return modules.flatMap((module) => {
|
||||
const operations = modules.flatMap((module) => {
|
||||
const paths = Array.isArray(module.paths) ? module.paths : [];
|
||||
return paths
|
||||
.filter((sourceRelativePath) => !isForeignPlatformPath(sourceRelativePath, 'grok'))
|
||||
// Root .mcp.json attach is owned by applyInstall's per-server filter.
|
||||
// Root MCP files are emitted only by the adapter's per-server consent operation.
|
||||
.filter((sourceRelativePath) => !isMcpPath(sourceRelativePath))
|
||||
.filter((sourceRelativePath) => allowHooks || !isHooksPath(sourceRelativePath))
|
||||
.map((sourceRelativePath) => adapter.createScaffoldOperation(
|
||||
module.id,
|
||||
sourceRelativePath,
|
||||
input
|
||||
));
|
||||
.map((sourceRelativePath) => {
|
||||
const operation = adapter.createScaffoldOperation(module.id, sourceRelativePath, input);
|
||||
return {
|
||||
...operation,
|
||||
destinationPath: path.join(adapter.resolveRoot(input), 'plugins', 'ecc', sourceRelativePath),
|
||||
...(normalizeRelativePath(sourceRelativePath) === 'hooks/hooks.json'
|
||||
? { contentTransform: 'grok-hook-boundary' }
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
});
|
||||
const hooksEnabled = allowHooks && operations.some((operation) => isHooksPath(operation.sourceRelativePath));
|
||||
const pluginRoot = path.join(adapter.resolveRoot(input), 'plugins', 'ecc');
|
||||
if (hooksEnabled) {
|
||||
operations.push({
|
||||
...adapter.createScaffoldOperation('hooks-runtime', 'hooks/hooks.json', input),
|
||||
destinationPath: path.join(pluginRoot, 'hooks', 'hooks.json'),
|
||||
contentTransform: 'grok-hook-boundary',
|
||||
});
|
||||
}
|
||||
operations.push({
|
||||
...adapter.createScaffoldOperation('grok-source-identity', '.ecc-source.json', input),
|
||||
destinationPath: path.join(pluginRoot, '.ecc-source.json'),
|
||||
});
|
||||
if (allowedMcpIds.length > 0) {
|
||||
operations.push({
|
||||
...adapter.createScaffoldOperation('grok-mcp', '.mcp.json', input),
|
||||
destinationPath: path.join(pluginRoot, '.mcp.json'),
|
||||
contentTransform: 'grok-mcp-consent',
|
||||
grokMcpIds: allowedMcpIds,
|
||||
});
|
||||
}
|
||||
const manifestOperation = adapter.createScaffoldOperation(
|
||||
'grok-manifest',
|
||||
'.grok-plugin/plugin.json',
|
||||
input
|
||||
);
|
||||
operations.push({
|
||||
...manifestOperation,
|
||||
destinationPath: path.join(pluginRoot, '.grok-plugin', 'plugin.json'),
|
||||
contentTransform: 'grok-plugin-consent',
|
||||
grokHooksEnabled: hooksEnabled,
|
||||
grokMcpEnabled: allowedMcpIds.length > 0,
|
||||
});
|
||||
return operations;
|
||||
},
|
||||
});
|
||||
@@ -318,6 +318,7 @@ function createInstallTargetAdapter(config) {
|
||||
id: config.id,
|
||||
target: config.target,
|
||||
kind: config.kind,
|
||||
acceptsUndeclaredModuleTargets: config.acceptsUndeclaredModuleTargets === true,
|
||||
nativeRootRelativePath: config.nativeRootRelativePath || null,
|
||||
supports(target) {
|
||||
return target === config.target || target === config.id;
|
||||
@@ -367,8 +368,13 @@ function createInstallTargetAdapter(config) {
|
||||
strategy: adapter.determineStrategy(normalizedSourcePath),
|
||||
});
|
||||
},
|
||||
excludesSourcePath(sourceRelativePath) {
|
||||
return isExcludedSourcePath(sourceRelativePath, config.excludedSourcePaths);
|
||||
prepareSource(input = {}) {
|
||||
if (typeof config.prepareSource === 'function') {
|
||||
return config.prepareSource(input, adapter);
|
||||
}
|
||||
return {
|
||||
sourceRoot: input.sourceRoot || input.repoRoot,
|
||||
};
|
||||
},
|
||||
planOperations(input = {}) {
|
||||
if (typeof config.planOperations === 'function') {
|
||||
|
||||
@@ -60,6 +60,9 @@ function planInstallTargetScaffold(options = {}) {
|
||||
projectRoot: options.projectRoot || options.repoRoot,
|
||||
homeDir: options.homeDir,
|
||||
env: resolveInvocationEnvironment(options),
|
||||
trust: options.trust === true,
|
||||
consent: options.consent || {},
|
||||
sourceSha: options.sourceSha || null,
|
||||
};
|
||||
const validationIssues = adapter.validate(planningInput);
|
||||
const blockingIssues = validationIssues.filter(issue => (
|
||||
|
||||
@@ -57,8 +57,7 @@ const {
|
||||
withoutStaleOperations,
|
||||
} = require('./stale-operations-reconciliation');
|
||||
const { buildInstallIndex, rewriteRelativeLinks } = require('./link-rewrite');
|
||||
const { transformInstallContent } = require('./content-transform');
|
||||
const { assertScriptBoundary, isScriptBoundary } = require('./opencode-script-boundary');
|
||||
const { transformCopyFileContent } = require('./content-transform');
|
||||
|
||||
function isMarkdownPath(filePath) {
|
||||
return /\.(md|mdx|markdown)$/i.test(String(filePath || ''));
|
||||
@@ -910,7 +909,7 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals
|
||||
&& isMarkdownPath(operation.destinationPath)
|
||||
);
|
||||
if (operation.kind === 'copy-file' && (operation.contentTransform || needsLinkRewrite)) {
|
||||
const transformed = transformInstallContent(
|
||||
const transformed = transformCopyFileContent(
|
||||
operation,
|
||||
fs.readFileSync(operation.sourcePath, 'utf8')
|
||||
);
|
||||
|
||||
@@ -1,59 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
const { stripPluginNamespace } = require('./plugin-namespace');
|
||||
const { adaptAntigravityAgent } = require('./antigravity-agent');
|
||||
const { adaptCopilotAgent } = require('./copilot-agent');
|
||||
const { disableOpenCodeHookPluginRegistration, getDisabledOpenCodePluginContent } = require('./hook-consent');
|
||||
|
||||
function rewriteCopilotWorkflowPaths(content, sourceRelativePath) {
|
||||
if (!/\.md$/i.test(sourceRelativePath)) return content;
|
||||
// Commands run from the installed project root. Rewrite only the supported
|
||||
// helper entrypoint, leaving unrelated project scripts and URLs untouched.
|
||||
return content.replace(/(^|[\s`"'(])scripts\/setup-package-manager\.js\b/g,
|
||||
'$1.github/ecc/scripts/setup-package-manager.js');
|
||||
function formatJson(value) {
|
||||
return `${JSON.stringify(value, null, 2)}\n`;
|
||||
}
|
||||
|
||||
function transformInstallContent(operation, content) {
|
||||
function transformCopyFileContent(operation, content) {
|
||||
if (!operation.contentTransform) {
|
||||
return content;
|
||||
}
|
||||
if (operation.contentTransform === 'claude-manual-plugin-namespace') {
|
||||
return stripPluginNamespace(content);
|
||||
}
|
||||
if (operation.contentTransform === 'qwen-agent-frontmatter') {
|
||||
return require('../../qwen-adapt-agents').adaptFrontmatter(content).text;
|
||||
}
|
||||
if (operation.contentTransform === 'antigravity-agent-frontmatter') {
|
||||
return adaptAntigravityAgent(content, operation.sourceRelativePath);
|
||||
}
|
||||
if (operation.contentTransform === 'copilot-agent-frontmatter') {
|
||||
return rewriteCopilotWorkflowPaths(adaptCopilotAgent(content, operation.sourceRelativePath), operation.sourceRelativePath);
|
||||
if (operation.contentTransform === 'grok-plugin-consent') {
|
||||
const manifest = JSON.parse(content);
|
||||
return formatJson({
|
||||
...manifest,
|
||||
hooks: operation.grokHooksEnabled ? 'hooks/hooks.json' : '',
|
||||
mcpServers: operation.grokMcpEnabled ? '.mcp.json' : '',
|
||||
});
|
||||
}
|
||||
if (operation.contentTransform === 'copilot-workflow-paths') {
|
||||
return rewriteCopilotWorkflowPaths(content, operation.sourceRelativePath);
|
||||
if (operation.contentTransform === 'grok-hook-boundary') {
|
||||
return content.split('var e=process.env.CLAUDE_PLUGIN_ROOT;').join(
|
||||
'var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT;'
|
||||
);
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-disable-ecc-hooks') {
|
||||
return transformInstallContent({ ...operation, contentTransform: 'opencode-home-skills-path' },
|
||||
disableOpenCodeHookPluginRegistration(content, operation.sourceRelativePath));
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-disable-plugin-entrypoint') {
|
||||
return getDisabledOpenCodePluginContent();
|
||||
}
|
||||
if (operation.contentTransform === 'opencode-home-skills-path') {
|
||||
if (operation.contentTransform === 'grok-mcp-consent') {
|
||||
const config = JSON.parse(content);
|
||||
if (!Array.isArray(config?.skills?.paths) || !config.skills.paths.includes('../skills')) {
|
||||
return content;
|
||||
if (!config || typeof config !== 'object' || Array.isArray(config)
|
||||
|| !config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
|
||||
throw new Error(`Invalid Grok MCP config: ${operation.sourceRelativePath}`);
|
||||
}
|
||||
const installedConfig = {
|
||||
const allowed = new Set(operation.grokMcpIds || []);
|
||||
return formatJson({
|
||||
...config,
|
||||
skills: {
|
||||
...config.skills,
|
||||
paths: config.skills.paths.map(skillPath => skillPath === '../skills' ? './skills' : skillPath),
|
||||
},
|
||||
};
|
||||
return `${JSON.stringify(installedConfig, null, 2)}\n`;
|
||||
mcpServers: Object.fromEntries(
|
||||
Object.entries(config.mcpServers).filter(([id]) => allowed.has(id))
|
||||
),
|
||||
});
|
||||
}
|
||||
throw new Error(`Unknown install content transform: ${operation.contentTransform}`);
|
||||
}
|
||||
|
||||
module.exports = { transformInstallContent };
|
||||
module.exports = {
|
||||
transformCopyFileContent,
|
||||
};
|
||||
@@ -103,6 +103,9 @@ function buildCopyFileOperation({
|
||||
destinationPath,
|
||||
strategy,
|
||||
contentTransform,
|
||||
grokHooksEnabled,
|
||||
grokMcpEnabled,
|
||||
grokMcpIds,
|
||||
}) {
|
||||
// Copilot's agent and workflow transforms are textual. Their directories may
|
||||
// also contain binary assets and executable helpers, which must keep bytes.
|
||||
@@ -118,7 +121,10 @@ function buildCopyFileOperation({
|
||||
strategy,
|
||||
ownership: 'managed',
|
||||
scaffoldOnly: false,
|
||||
...(applicableTransform ? { contentTransform: applicableTransform } : {}),
|
||||
...(contentTransform ? { contentTransform } : {}),
|
||||
...(contentTransform === 'grok-plugin-consent' ? { grokHooksEnabled } : {}),
|
||||
...(contentTransform === 'grok-plugin-consent' ? { grokMcpEnabled } : {}),
|
||||
...(contentTransform === 'grok-mcp-consent' ? { grokMcpIds: [...grokMcpIds] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,6 +204,9 @@ function materializeScaffoldOperation(sourceRoot, operation) {
|
||||
destinationPath: operation.destinationPath,
|
||||
strategy: operation.strategy,
|
||||
contentTransform: operation.contentTransform,
|
||||
grokHooksEnabled: operation.grokHooksEnabled,
|
||||
grokMcpEnabled: operation.grokMcpEnabled,
|
||||
grokMcpIds: operation.grokMcpIds,
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -215,6 +224,9 @@ function materializeScaffoldOperation(sourceRoot, operation) {
|
||||
destinationPath: path.join(operation.destinationPath, relativeFile),
|
||||
strategy: operation.strategy,
|
||||
contentTransform: operation.contentTransform,
|
||||
grokHooksEnabled: operation.grokHooksEnabled,
|
||||
grokMcpEnabled: operation.grokMcpEnabled,
|
||||
grokMcpIds: operation.grokMcpIds,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -256,9 +268,20 @@ function dedupeCopyFileOperations(operations) {
|
||||
}
|
||||
|
||||
function createManifestInstallPlan(options = {}) {
|
||||
const sourceRoot = options.sourceRoot || getSourceRoot();
|
||||
const requestedSourceRoot = options.sourceRoot || getSourceRoot();
|
||||
const projectRoot = options.projectRoot || process.cwd();
|
||||
const target = options.target || 'claude';
|
||||
const adapter = getInstallTargetAdapter(target);
|
||||
const preparedSource = adapter.prepareSource({
|
||||
sourceRoot: requestedSourceRoot,
|
||||
repoRoot: requestedSourceRoot,
|
||||
projectRoot,
|
||||
homeDir: options.homeDir,
|
||||
env: resolveInvocationEnvironment(options),
|
||||
sourceUrl: options.sourceUrl || null,
|
||||
sourceSha: options.sourceSha || null,
|
||||
});
|
||||
const sourceRoot = preparedSource.sourceRoot || requestedSourceRoot;
|
||||
const legacyLanguages = Array.isArray(options.legacyLanguages) ? [...options.legacyLanguages] : [];
|
||||
const requestProfileId = Object.hasOwn(options, 'requestProfileId') ? options.requestProfileId : options.profileId || null;
|
||||
const requestModuleIds = Object.hasOwn(options, 'requestModuleIds') ? [...options.requestModuleIds] : Array.isArray(options.moduleIds) ? [...options.moduleIds] : [];
|
||||
@@ -283,8 +306,10 @@ function createManifestInstallPlan(options = {}) {
|
||||
excludeComponentIds: options.excludeComponentIds || [],
|
||||
target,
|
||||
exemptValidationCodes: options.exemptValidationCodes || [],
|
||||
trust: options.trust === true,
|
||||
consent: options.consent || {},
|
||||
sourceSha: options.sourceSha || null,
|
||||
});
|
||||
const adapter = getInstallTargetAdapter(target);
|
||||
const materializedOperations = plan.operations.flatMap(operation => (
|
||||
materializeScaffoldOperation(sourceRoot, operation)
|
||||
)).map(operation => {
|
||||
@@ -307,8 +332,9 @@ function createManifestInstallPlan(options = {}) {
|
||||
: materializedOperations
|
||||
);
|
||||
const source = {
|
||||
repoVersion: getPackageVersion(sourceRoot),
|
||||
repoCommit: getRepoCommit(sourceRoot),
|
||||
repoVersion: preparedSource.sourceVersion || getPackageVersion(sourceRoot),
|
||||
repoUrl: preparedSource.sourceUrl || null,
|
||||
repoCommit: preparedSource.sourceSha || getRepoCommit(sourceRoot),
|
||||
manifestVersion: getManifestVersion(sourceRoot)
|
||||
};
|
||||
const statePreview = createStatePreview({
|
||||
@@ -321,7 +347,11 @@ function createManifestInstallPlan(options = {}) {
|
||||
includeComponents: requestIncludeComponentIds,
|
||||
excludeComponents: requestExcludeComponentIds,
|
||||
legacyLanguages,
|
||||
legacyMode: Boolean(options.legacyMode)
|
||||
legacyMode: Boolean(options.legacyMode),
|
||||
trust: options.trust === true,
|
||||
consentMcp: Object.entries(options.consent && options.consent.mcp || {})
|
||||
.filter(([, allowed]) => allowed === true)
|
||||
.map(([id]) => id),
|
||||
},
|
||||
resolution: {
|
||||
selectedModules: plan.selectedModuleIds,
|
||||
@@ -331,7 +361,7 @@ function createManifestInstallPlan(options = {}) {
|
||||
source
|
||||
});
|
||||
|
||||
return disableUnselectedOpenCodeHooks({
|
||||
const installPlan = {
|
||||
mode: options.mode || 'manifest',
|
||||
sourceRoot,
|
||||
target,
|
||||
@@ -357,7 +387,15 @@ function createManifestInstallPlan(options = {}) {
|
||||
excludedModuleIds: plan.excludedModuleIds,
|
||||
operations,
|
||||
statePreview
|
||||
});
|
||||
};
|
||||
if (typeof preparedSource.cleanup === 'function') {
|
||||
Object.defineProperty(installPlan, 'dispose', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: preparedSource.cleanup,
|
||||
});
|
||||
}
|
||||
return installPlan;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -32,6 +32,10 @@ function parseInstallArgs(argv) {
|
||||
locale: null,
|
||||
enableHooks: false,
|
||||
noHooks: false,
|
||||
trust: false,
|
||||
consentHooks: false,
|
||||
consentMcp: [],
|
||||
sourceSha: null,
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
@@ -76,6 +80,16 @@ function parseInstallArgs(argv) {
|
||||
parsed.enableHooks = true;
|
||||
} else if (arg === '--no-hooks') {
|
||||
parsed.noHooks = true;
|
||||
} else if (arg === '--trust') {
|
||||
parsed.trust = true;
|
||||
} else if (arg === '--consent-hooks') {
|
||||
parsed.consentHooks = true;
|
||||
} else if (arg === '--consent-mcp') {
|
||||
parsed.consentMcp = dedupeStrings((args[index + 1] || '').split(','));
|
||||
index += 1;
|
||||
} else if (arg === '--source-sha') {
|
||||
parsed.sourceSha = args[index + 1] || null;
|
||||
index += 1;
|
||||
} else if (arg === '--dry-run') {
|
||||
parsed.dryRun = true;
|
||||
} else if (arg === '--json') {
|
||||
@@ -134,7 +148,16 @@ function normalizeInstallRequest(options = {}) {
|
||||
...(Array.isArray(options.legacyLanguages) ? options.legacyLanguages : []),
|
||||
...(Array.isArray(options.languages) ? options.languages : []),
|
||||
]).map(language => language.toLowerCase()));
|
||||
const hookConsent = resolveHookConsentFlags(options);
|
||||
const hookConsent = resolveHookConsentFlags({
|
||||
...options,
|
||||
enableHooks: options.enableHooks === true || options.consentHooks === true,
|
||||
});
|
||||
const trust = options.trust === true;
|
||||
const consentMcp = dedupeStrings(options.consentMcp || []);
|
||||
const sourceSha = options.sourceSha || null;
|
||||
if (sourceSha && (target !== 'grok' || !/^[0-9a-f]{40}$/.test(sourceSha))) {
|
||||
throw new Error('--source-sha requires --target grok and a 40-character lowercase commit SHA');
|
||||
}
|
||||
if (hookConsent === 'declined' && moduleIds.includes('hooks-runtime')) {
|
||||
throw new Error('--no-hooks cannot be combined with an explicit hooks-runtime module selection');
|
||||
}
|
||||
@@ -167,6 +190,9 @@ function normalizeInstallRequest(options = {}) {
|
||||
excludeComponentIds,
|
||||
legacyLanguages,
|
||||
hookConsent,
|
||||
trust,
|
||||
consentMcp,
|
||||
sourceSha,
|
||||
configPath: config?.path || options.configPath || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ function createRawInstallPlan(request, options = {}) {
|
||||
homeDir: options.homeDir,
|
||||
env: resolveInvocationEnvironment(options),
|
||||
sourceRoot: options.sourceRoot,
|
||||
sourceUrl: options.sourceUrl,
|
||||
sourceSha: options.sourceSha || request.sourceSha,
|
||||
trust: request.trust,
|
||||
consent: {
|
||||
hooks: request.hookConsent === 'enabled',
|
||||
mcp: Object.fromEntries((request.consentMcp || []).map(id => [id, true])),
|
||||
},
|
||||
exemptValidationCodes: options.exemptValidationCodes || [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,33 +29,75 @@ const PLUGIN_ROOT_SEGMENTS = [
|
||||
// If that skill is ever renamed, move this sentinel with it.
|
||||
const DEFAULT_SCRIPT_PROBE = path.join('scripts', 'lib', 'utils.js');
|
||||
const DEFAULT_SKILL_PROBE = path.join('skills', 'continuous-learning-v2');
|
||||
const PLUGIN_ROOT_ENV_KEYS = ['GROK_PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT', 'ECC_PLUGIN_ROOT'];
|
||||
|
||||
function pluginRootFromEnv(env = process.env) {
|
||||
for (const key of PLUGIN_ROOT_ENV_KEYS) {
|
||||
const value = env[key];
|
||||
if (value && String(value).trim()) {
|
||||
return String(value).trim();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* Resolve the ECC source root directory.
|
||||
*
|
||||
* Tries, in order:
|
||||
* 1. CLAUDE_PLUGIN_ROOT env var (set by Claude Code for hooks, or by user)
|
||||
* 2. Standard install location (~/.claude/) — when scripts exist there
|
||||
* 3. Known plugin roots under ~/.claude/plugins/ (current + legacy slugs)
|
||||
* 4. Plugin cache auto-detection — scans ~/.claude/plugins/cache/{ecc,everything-claude-code}/
|
||||
* 5. Fallback to ~/.claude/ (original behaviour)
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.homeDir] Override home directory (for testing)
|
||||
* @param {string} [options.envRoot] Override CLAUDE_PLUGIN_ROOT (for testing)
|
||||
* @param {string} [options.probe] Relative path used to verify a candidate
|
||||
* root contains what the caller needs. When
|
||||
* given, it is honored exactly (script
|
||||
* consumers pass their own script path). When
|
||||
* omitted, a candidate must contain BOTH the
|
||||
* ECC script tree and a sentinel ECC skill,
|
||||
* so a partial install (scripts without
|
||||
* skills) is rejected for skill consumers.
|
||||
* @returns {string} Resolved ECC root path
|
||||
*/
|
||||
function resolveEccRoot(options = {}) {
|
||||
const envRoot = options.envRoot !== undefined
|
||||
? options.envRoot
|
||||
: (process.env.CLAUDE_PLUGIN_ROOT || '');
|
||||
|
||||
function findRootInVendorHome(vendorDir, isRoot) {
|
||||
if (isRoot(vendorDir)) {
|
||||
return vendorDir;
|
||||
if (envRoot && envRoot.trim()) {
|
||||
return envRoot.trim();
|
||||
}
|
||||
|
||||
for (const segments of PLUGIN_ROOT_SEGMENTS) {
|
||||
const candidate = path.join(vendorDir, 'plugins', ...segments);
|
||||
const homeDir = options.homeDir || os.homedir();
|
||||
const claudeDir = path.join(homeDir, '.claude');
|
||||
|
||||
// Decide whether a candidate directory is a usable ECC root. An explicit
|
||||
// caller probe is honored exactly (script consumers know the artifact they
|
||||
// need). With the default probe the caller is a skill consumer, so a
|
||||
// candidate must contain both ECC's scripts and a sentinel ECC skill —
|
||||
// otherwise a scripts-only ~/.claude short-circuits and every skill path
|
||||
// resolves to a location that does not exist (#2544).
|
||||
const isRoot = options.probe
|
||||
? (dir) => fs.existsSync(path.join(dir, options.probe))
|
||||
: (dir) => fs.existsSync(path.join(dir, DEFAULT_SCRIPT_PROBE))
|
||||
&& fs.existsSync(path.join(dir, DEFAULT_SKILL_PROBE));
|
||||
|
||||
// Standard install — files are copied directly into ~/.claude/
|
||||
if (isRoot(claudeDir)) {
|
||||
return claudeDir;
|
||||
}
|
||||
|
||||
// Exact legacy plugin install locations. These preserve backwards
|
||||
// compatibility without scanning arbitrary plugin trees.
|
||||
const legacyPluginRoots = PLUGIN_ROOT_SEGMENTS.map((segments) =>
|
||||
path.join(claudeDir, 'plugins', ...segments)
|
||||
);
|
||||
|
||||
for (const candidate of legacyPluginRoots) {
|
||||
if (isRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin cache — Claude Code stores marketplace plugins under
|
||||
// ~/.claude/plugins/cache/<plugin-name>/<org>/<version>/
|
||||
try {
|
||||
for (const slug of PLUGIN_CACHE_SLUGS) {
|
||||
const cacheBase = path.join(vendorDir, 'plugins', 'cache', slug);
|
||||
const cacheBase = path.join(claudeDir, 'plugins', 'cache', slug);
|
||||
const orgDirs = fs.readdirSync(cacheBase, { withFileTypes: true });
|
||||
|
||||
for (const orgEntry of orgDirs) {
|
||||
@@ -79,77 +121,10 @@ function findRootInVendorHome(vendorDir, isRoot) {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Vendor plugin cache doesn't exist or isn't readable.
|
||||
// Plugin cache doesn't exist or isn't readable — continue to fallback
|
||||
}
|
||||
|
||||
try {
|
||||
const installedBase = path.join(vendorDir, 'installed-plugins');
|
||||
const installedDirs = fs.readdirSync(installedBase, { withFileTypes: true });
|
||||
for (const entry of installedDirs) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const candidate = path.join(installedBase, entry.name);
|
||||
if (isRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Grok (and similar) installed-plugins homes are optional.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the ECC source root directory.
|
||||
*
|
||||
* Tries, in order:
|
||||
* 1. GROK_PLUGIN_ROOT, CLAUDE_PLUGIN_ROOT, or ECC_PLUGIN_ROOT
|
||||
* 2. Standard install location (~/.claude/) — when a complete root exists there
|
||||
* 3. Known plugin roots under ~/.claude/plugins/ (current + legacy slugs)
|
||||
* 4. Plugin cache auto-detection — scans ~/.claude/plugins/cache/{ecc,everything-claude-code}/
|
||||
* 5. The same layout under ~/.grok/, plus ~/.grok/installed-plugins/<hash>/
|
||||
* 6. Fallback to ~/.claude/ (original behaviour)
|
||||
*
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.homeDir] Override home directory (for testing)
|
||||
* @param {string} [options.envRoot] Override plugin-root env (for testing)
|
||||
* @param {string} [options.probe] Relative path used to verify a candidate
|
||||
* root contains what the caller needs. When
|
||||
* given, it is honored exactly (script
|
||||
* consumers pass their own script path). When
|
||||
* omitted, a candidate must contain BOTH the
|
||||
* ECC script tree and a sentinel ECC skill,
|
||||
* so a partial install (scripts without
|
||||
* skills) is rejected for skill consumers.
|
||||
* @returns {string} Resolved ECC root path
|
||||
*/
|
||||
function resolveEccRoot(options = {}) {
|
||||
const envRoot = options.envRoot !== undefined
|
||||
? options.envRoot
|
||||
: pluginRootFromEnv(process.env);
|
||||
|
||||
if (envRoot && envRoot.trim()) {
|
||||
return envRoot.trim();
|
||||
}
|
||||
|
||||
const homeDir = options.homeDir || os.homedir();
|
||||
const claudeDir = path.join(homeDir, '.claude');
|
||||
const grokDir = path.join(homeDir, '.grok');
|
||||
|
||||
// Decide whether a candidate directory is a usable ECC root. An explicit
|
||||
// caller probe is honored exactly (script consumers know the artifact they
|
||||
// need). With the default probe the caller is a skill consumer, so a
|
||||
// candidate must contain both ECC's scripts and a sentinel ECC skill —
|
||||
// otherwise a scripts-only ~/.claude short-circuits and every skill path
|
||||
// resolves to a location that does not exist (#2544).
|
||||
const isRoot = options.probe
|
||||
? (dir) => fs.existsSync(path.join(dir, options.probe))
|
||||
: (dir) => fs.existsSync(path.join(dir, DEFAULT_SCRIPT_PROBE))
|
||||
&& fs.existsSync(path.join(dir, DEFAULT_SKILL_PROBE));
|
||||
|
||||
return findRootInVendorHome(claudeDir, isRoot)
|
||||
|| findRootInVendorHome(grokDir, isRoot)
|
||||
|| claudeDir;
|
||||
return claudeDir;
|
||||
}
|
||||
|
||||
function normalizePluginRootForPlatform(rootDir, platform = process.platform) {
|
||||
@@ -171,21 +146,21 @@ function normalizePluginRootForPlatform(rootDir, platform = process.platform) {
|
||||
*
|
||||
* This minified form contains no spread, no nested array literals, and no
|
||||
* escaped double quotes, so it survives `node -e "..."` quoting on every shell.
|
||||
* When GROK_PLUGIN_ROOT, CLAUDE_PLUGIN_ROOT, or ECC_PLUGIN_ROOT is set it is
|
||||
* used directly. Grok trusted plugin hooks set both GROK_PLUGIN_ROOT and the
|
||||
* CLAUDE_PLUGIN_ROOT alias. Otherwise the inline probes ~/.claude plugin
|
||||
* locations only far enough to load this module, then delegates to
|
||||
* resolveEccRoot() (which also searches ~/.grok).
|
||||
* When CLAUDE_PLUGIN_ROOT is set (as Claude Code does for plugin hooks and
|
||||
* commands) it is used directly. Otherwise the inline probes the same set of
|
||||
* locations resolveEccRoot() knows about — ~/.claude, the exact plugin roots
|
||||
* under ~/.claude/plugins/, and the versioned plugin cache — only far enough to
|
||||
* load the committed resolve-ecc-root module, then delegates the authoritative
|
||||
* decision to resolveEccRoot(). This keeps discovery behaviour identical to the
|
||||
* old inline while centralising the real logic in one tested module.
|
||||
*
|
||||
* Usage in commands:
|
||||
* const _r = <paste INLINE_RESOLVE>;
|
||||
* const sm = require(_r + '/scripts/lib/session-manager');
|
||||
*/
|
||||
const INLINE_RESOLVE = `(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.GROK_PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()`;
|
||||
const INLINE_RESOLVE = `(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})()`;
|
||||
|
||||
module.exports = {
|
||||
resolveEccRoot,
|
||||
pluginRootFromEnv,
|
||||
PLUGIN_ROOT_ENV_KEYS,
|
||||
INLINE_RESOLVE,
|
||||
};
|
||||
@@ -83,19 +83,6 @@ function writePluginMetadata(root, counts) {
|
||||
description: `Fixture marketplace plugin — ${counts.agents} agents, ${counts.skills} skills, ${counts.commands} legacy command shims`,
|
||||
}],
|
||||
}, null, 2));
|
||||
|
||||
const grokDir = path.join(root, '.grok-plugin');
|
||||
fs.mkdirSync(grokDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(grokDir, 'plugin.json'), JSON.stringify({
|
||||
name: 'ecc',
|
||||
description: `Fixture Grok plugin — ${counts.agents} agents, ${counts.skills} skills, ${counts.commands} legacy command shims`,
|
||||
}, null, 2));
|
||||
fs.writeFileSync(path.join(grokDir, 'marketplace.json'), JSON.stringify({
|
||||
plugins: [{
|
||||
name: 'ecc',
|
||||
description: `Fixture Grok marketplace plugin — ${counts.agents} agents, ${counts.skills} skills, ${counts.commands} legacy command shims`,
|
||||
}],
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
function writeEnglishAgents(root, counts, options = {}) {
|
||||
@@ -297,8 +284,6 @@ function runTests() {
|
||||
assert.ok(formatted.includes('.gemini/GEMINI.md'));
|
||||
assert.ok(formatted.includes('.claude-plugin/plugin.json description'));
|
||||
assert.ok(formatted.includes('.claude-plugin/marketplace.json plugin description'));
|
||||
assert.ok(formatted.includes('.grok-plugin/plugin.json description'));
|
||||
assert.ok(formatted.includes('.grok-plugin/marketplace.json plugin description'));
|
||||
assert.ok(formatted.includes('README.zh-CN.md quick-start summary'));
|
||||
assert.ok(formatted.includes('docs/zh-CN/README.md parity table'));
|
||||
assert.ok(formatted.includes('docs/zh-CN/AGENTS.md project structure'));
|
||||
@@ -329,8 +314,6 @@ function runTests() {
|
||||
const zhAgentsDoc = fs.readFileSync(path.join(testDir, 'docs', 'zh-CN', 'AGENTS.md'), 'utf8');
|
||||
const pluginJson = fs.readFileSync(path.join(testDir, '.claude-plugin', 'plugin.json'), 'utf8');
|
||||
const marketplaceJson = fs.readFileSync(path.join(testDir, '.claude-plugin', 'marketplace.json'), 'utf8');
|
||||
const grokPluginJson = fs.readFileSync(path.join(testDir, '.grok-plugin', 'plugin.json'), 'utf8');
|
||||
const grokMarketplaceJson = fs.readFileSync(path.join(testDir, '.grok-plugin', 'marketplace.json'), 'utf8');
|
||||
|
||||
assert.ok(readme.includes('Access to 1 agents, 1 skills, and 1 legacy command shims'));
|
||||
assert.ok(readme.includes('actual OSS surface: 7 agents, 7 skills, and 7 legacy command shims'));
|
||||
@@ -346,8 +329,6 @@ function runTests() {
|
||||
assert.ok(zhAgentsDoc.includes('skills/ - 1+ 个工作流技能和领域知识'));
|
||||
assert.ok(pluginJson.includes('1 agents, 1 skills, 1 legacy command shims'));
|
||||
assert.ok(marketplaceJson.includes('1 agents, 1 skills, 1 legacy command shims'));
|
||||
assert.ok(grokPluginJson.includes('1 agents, 1 skills, 1 legacy command shims'));
|
||||
assert.ok(grokMarketplaceJson.includes('1 agents, 1 skills, 1 legacy command shims'));
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ function runTests() {
|
||||
ECC_HOOKS_ENABLED: undefined,
|
||||
CLAUDE_PLUGIN_OPTION_HOOKS_ENABLED: undefined,
|
||||
ECC_HOOK_CONFIG: undefined,
|
||||
GROK_PLUGIN_ROOT: undefined,
|
||||
CLAUDE_PLUGIN_ROOT: undefined,
|
||||
ECC_PLUGIN_ROOT: undefined,
|
||||
}, () => {
|
||||
@@ -99,36 +98,6 @@ function runTests() {
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('GROK_PLUGIN_ROOT locates managed hook config when Claude root is unset', () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-hook-flags-grok-'));
|
||||
const configPath = path.join(root, 'ecc', 'setup.json');
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, JSON.stringify({
|
||||
hooks: { enabled: false, profile: 'minimal' },
|
||||
}));
|
||||
withEnv({
|
||||
ECC_HOOKS_ENABLED: undefined,
|
||||
ECC_HOOK_PROFILE: undefined,
|
||||
CLAUDE_PLUGIN_OPTION_HOOKS_ENABLED: undefined,
|
||||
CLAUDE_PLUGIN_OPTION_HOOK_PROFILE: undefined,
|
||||
ECC_HOOK_CONFIG: undefined,
|
||||
CLAUDE_PLUGIN_ROOT: undefined,
|
||||
ECC_PLUGIN_ROOT: undefined,
|
||||
GROK_PLUGIN_ROOT: root,
|
||||
}, () => {
|
||||
assert.deepStrictEqual(readManagedHookConfig(), {
|
||||
enabled: false,
|
||||
profile: 'minimal',
|
||||
});
|
||||
assert.strictEqual(areHooksEnabled(), false);
|
||||
assert.strictEqual(getHookProfile(), 'minimal');
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('Claude plugin options control enabled state and profile', () => {
|
||||
withEnv({
|
||||
ECC_HOOKS_ENABLED: undefined,
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Canonical Grok install contract tests.
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
|
||||
const { applyInstallPlan, previewInstallPlan } = require('../../scripts/lib/install-executor');
|
||||
const { repairInstalledStates, uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
||||
const { createInstallPlanFromRequest } = require('../../scripts/lib/install/runtime');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${error.stack || error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function git(cwd, args, options = {}) {
|
||||
return execFileSync('git', args, {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
...options,
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function commit(root, message) {
|
||||
git(root, ['add', '.']);
|
||||
git(root, ['-c', 'user.email=grok-canonical@test', '-c', 'user.name=GrokCanonical', 'commit', '-m', message]);
|
||||
return git(root, ['rev-parse', 'HEAD']);
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function createPinnedFixture() {
|
||||
const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-grok-canonical-'));
|
||||
const sourceRoot = path.join(parent, 'source');
|
||||
const homeDir = path.join(parent, 'home');
|
||||
git(parent, ['clone', '--quiet', '--no-local', repoRoot, sourceRoot]);
|
||||
git(sourceRoot, ['remote', 'add', 'pinned-source', sourceRoot]);
|
||||
fs.mkdirSync(homeDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(sourceRoot, 'commands', 'plan.md'), 'pinned payload\n');
|
||||
writeJson(path.join(sourceRoot, '.mcp.json'), {
|
||||
mcpServers: {
|
||||
'chrome-devtools': { command: 'npx', args: ['-y', 'chrome-devtools-mcp@latest'] },
|
||||
'other-mcp': { command: 'node', args: ['mcp.js'] },
|
||||
},
|
||||
});
|
||||
const pluginManifestPath = path.join(sourceRoot, '.grok-plugin', 'plugin.json');
|
||||
const pluginManifest = JSON.parse(fs.readFileSync(pluginManifestPath, 'utf8'));
|
||||
writeJson(pluginManifestPath, { ...pluginManifest, version: '2.2.1' });
|
||||
const payloadSha = commit(sourceRoot, 'fixture payload');
|
||||
|
||||
const marketplacePath = path.join(sourceRoot, '.grok-plugin', 'marketplace.json');
|
||||
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, 'utf8'));
|
||||
marketplace.plugins[0] = {
|
||||
...marketplace.plugins[0],
|
||||
version: '2.2.1',
|
||||
source: {
|
||||
source: 'url',
|
||||
url: sourceRoot,
|
||||
sha: payloadSha,
|
||||
},
|
||||
};
|
||||
writeJson(marketplacePath, marketplace);
|
||||
commit(sourceRoot, 'fixture catalog');
|
||||
|
||||
return { parent, sourceRoot, homeDir, payloadSha };
|
||||
}
|
||||
|
||||
function request({ trust = true, hooks = true, mcp = ['other-mcp'] } = {}) {
|
||||
return {
|
||||
mode: 'manifest',
|
||||
target: 'grok',
|
||||
profileId: null,
|
||||
moduleIds: ['commands-core', 'hooks-runtime'],
|
||||
includeComponentIds: [],
|
||||
excludeComponentIds: [],
|
||||
legacyLanguages: [],
|
||||
hookConsent: hooks ? 'enabled' : 'declined',
|
||||
trust,
|
||||
consentMcp: mcp,
|
||||
};
|
||||
}
|
||||
|
||||
function createPlan(fixture, requestOptions = {}, extra = {}) {
|
||||
return createInstallPlanFromRequest(request(requestOptions), {
|
||||
sourceRoot: fixture.sourceRoot,
|
||||
projectRoot: fixture.sourceRoot,
|
||||
homeDir: fixture.homeDir,
|
||||
env: {},
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
function installedRoot(fixture) {
|
||||
return path.join(fixture.homeDir, '.grok', 'plugins', 'ecc');
|
||||
}
|
||||
|
||||
function inspectWithGrok(fixture) {
|
||||
const projectRoot = path.join(fixture.parent, 'project');
|
||||
fs.mkdirSync(projectRoot, { recursive: true });
|
||||
const result = spawnSync('grok', ['inspect', '--json'], {
|
||||
cwd: projectRoot,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: fixture.homeDir,
|
||||
GROK_HOME: path.join(fixture.homeDir, '.grok'),
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
if (result.error && result.error.code === 'ENOENT') return null;
|
||||
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
||||
return JSON.parse(result.stdout);
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Canonical Grok install contract ===\n');
|
||||
|
||||
if (test('preview and apply use one canonical operation set with explicit capability consent', () => {
|
||||
const fixture = createPinnedFixture();
|
||||
try {
|
||||
const rawPlan = createPlan(fixture);
|
||||
const preview = previewInstallPlan(rawPlan);
|
||||
const applied = applyInstallPlan(rawPlan);
|
||||
const state = JSON.parse(fs.readFileSync(rawPlan.installStatePath, 'utf8'));
|
||||
|
||||
assert.deepStrictEqual(applied.plannedOperations, preview.plannedOperations);
|
||||
assert.deepStrictEqual(
|
||||
state.operations.map(operation => operation.destinationPath),
|
||||
preview.plannedOperations.map(operation => operation.destinationPath)
|
||||
);
|
||||
assert.strictEqual(state.request.trust, true);
|
||||
assert.deepStrictEqual(state.request.consentMcp, ['other-mcp']);
|
||||
assert.strictEqual(state.source.repoCommit, fixture.payloadSha);
|
||||
assert.strictEqual(state.source.repoUrl, fixture.sourceRoot);
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(installedRoot(fixture), '.grok-plugin', 'plugin.json'), 'utf8'));
|
||||
const mcp = JSON.parse(fs.readFileSync(path.join(installedRoot(fixture), '.mcp.json'), 'utf8'));
|
||||
assert.strictEqual(manifest.hooks, 'hooks/hooks.json');
|
||||
assert.strictEqual(manifest.mcpServers, '.mcp.json');
|
||||
assert.deepStrictEqual(Object.keys(mcp.mcpServers), ['other-mcp']);
|
||||
assert.ok(!JSON.stringify(mcp).includes('chrome-devtools'));
|
||||
const installedHooks = fs.readFileSync(path.join(installedRoot(fixture), 'hooks', 'hooks.json'), 'utf8');
|
||||
assert.ok(installedHooks.includes('GROK_PLUGIN_ROOT'));
|
||||
assert.ok(!fs.readFileSync(path.join(repoRoot, 'hooks', 'hooks.json'), 'utf8').includes('GROK_PLUGIN_ROOT'));
|
||||
const inspection = inspectWithGrok(fixture);
|
||||
if (inspection) {
|
||||
const plugin = inspection.plugins.find((item) => item.name === 'ecc');
|
||||
assert.ok(plugin && plugin.enabled);
|
||||
assert.strictEqual(plugin.provides.hooks, true);
|
||||
assert.strictEqual(plugin.provides.mcpServers, 1);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(fixture.parent, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('untrusted and denied plans omit hooks and every MCP capability', () => {
|
||||
const fixture = createPinnedFixture();
|
||||
try {
|
||||
for (const options of [
|
||||
{ trust: false, hooks: true, mcp: ['chrome-devtools', 'other-mcp'] },
|
||||
{ trust: true, hooks: false, mcp: [] },
|
||||
]) {
|
||||
fs.rmSync(path.join(fixture.homeDir, '.grok'), { recursive: true, force: true });
|
||||
const plan = createPlan(fixture, options);
|
||||
applyInstallPlan(plan);
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(installedRoot(fixture), '.grok-plugin', 'plugin.json'), 'utf8'));
|
||||
assert.strictEqual(manifest.hooks, '');
|
||||
assert.strictEqual(manifest.mcpServers, '');
|
||||
assert.ok(!fs.existsSync(path.join(installedRoot(fixture), '.mcp.json')));
|
||||
assert.ok(!fs.existsSync(path.join(installedRoot(fixture), 'hooks', 'hooks.json')));
|
||||
const inspection = inspectWithGrok(fixture);
|
||||
if (inspection) {
|
||||
const plugin = inspection.plugins.find((item) => item.name === 'ecc');
|
||||
assert.ok(plugin && plugin.enabled);
|
||||
assert.strictEqual(plugin.provides.hooks, false);
|
||||
assert.strictEqual(plugin.provides.mcpServers, 0);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(fixture.parent, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('canonical source preparation installs the pinned commit, not HEAD or dirty files', () => {
|
||||
const fixture = createPinnedFixture();
|
||||
try {
|
||||
fs.writeFileSync(path.join(fixture.sourceRoot, 'commands', 'plan.md'), 'dirty payload\n');
|
||||
fs.writeFileSync(path.join(fixture.sourceRoot, 'untracked.txt'), 'do not install\n');
|
||||
const plan = createPlan(fixture, { hooks: false, mcp: [] });
|
||||
applyInstallPlan(plan);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(path.join(installedRoot(fixture), 'commands', 'plan.md'), 'utf8'),
|
||||
'pinned payload\n'
|
||||
);
|
||||
assert.ok(!fs.existsSync(path.join(installedRoot(fixture), 'untracked.txt')));
|
||||
} finally {
|
||||
fs.rmSync(fixture.parent, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('source identity preserves case-sensitive remote paths', () => {
|
||||
const fixture = createPinnedFixture();
|
||||
try {
|
||||
const marketplacePath = path.join(fixture.sourceRoot, '.grok-plugin', 'marketplace.json');
|
||||
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, 'utf8'));
|
||||
marketplace.plugins[0].source.url = 'https://example.com/Owner/ECC.git';
|
||||
writeJson(marketplacePath, marketplace);
|
||||
assert.throws(
|
||||
() => createPlan(
|
||||
fixture,
|
||||
{ hooks: false, mcp: [] },
|
||||
{ sourceUrl: 'https://example.com/owner/ECC.git' }
|
||||
),
|
||||
/source URL does not match/
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(fixture.parent, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('source identity rejects a marketplace version that does not match the pinned plugin', () => {
|
||||
const fixture = createPinnedFixture();
|
||||
try {
|
||||
const marketplacePath = path.join(fixture.sourceRoot, '.grok-plugin', 'marketplace.json');
|
||||
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, 'utf8'));
|
||||
marketplace.plugins[0].version = '9.9.9';
|
||||
writeJson(marketplacePath, marketplace);
|
||||
assert.throws(
|
||||
() => createPlan(fixture, { hooks: false, mcp: [] }),
|
||||
/plugin version 2\.2\.1 does not match marketplace version 9\.9\.9/
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(fixture.parent, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('marketplace-extracted trees require matching Grok registry evidence and refetch the pin', () => {
|
||||
const fixture = createPinnedFixture();
|
||||
const extractedRoot = path.join(fixture.parent, 'extracted');
|
||||
try {
|
||||
fs.mkdirSync(path.join(extractedRoot, '.grok-plugin'), { recursive: true });
|
||||
fs.copyFileSync(
|
||||
path.join(fixture.sourceRoot, '.grok-plugin', 'marketplace.json'),
|
||||
path.join(extractedRoot, '.grok-plugin', 'marketplace.json')
|
||||
);
|
||||
assert.throws(
|
||||
() => createInstallPlanFromRequest(request(), {
|
||||
sourceRoot: extractedRoot,
|
||||
projectRoot: extractedRoot,
|
||||
homeDir: fixture.homeDir,
|
||||
env: {},
|
||||
}),
|
||||
/registry|verified source|unverifiable/i
|
||||
);
|
||||
|
||||
writeJson(path.join(fixture.homeDir, '.grok', 'installed-plugins', 'registry.json'), {
|
||||
version: 1,
|
||||
repos: {
|
||||
ecc: {
|
||||
path: extractedRoot,
|
||||
kind: { type: 'Git', url: fixture.sourceRoot, commit: fixture.payloadSha },
|
||||
plugins: { ecc: { version: '2.2.1' } },
|
||||
},
|
||||
},
|
||||
});
|
||||
const plan = createInstallPlanFromRequest(request({ hooks: false, mcp: [] }), {
|
||||
sourceRoot: extractedRoot,
|
||||
projectRoot: extractedRoot,
|
||||
homeDir: fixture.homeDir,
|
||||
env: {},
|
||||
});
|
||||
applyInstallPlan(plan);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(path.join(installedRoot(fixture), 'commands', 'plan.md'), 'utf8'),
|
||||
'pinned payload\n'
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(fixture.parent, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('repair, uninstall, upgrade, and rollback stay on canonical install state', () => {
|
||||
const fixture = createPinnedFixture();
|
||||
try {
|
||||
const firstPlan = createPlan(fixture, { hooks: false, mcp: [] });
|
||||
applyInstallPlan(firstPlan);
|
||||
const firstState = JSON.parse(fs.readFileSync(firstPlan.installStatePath, 'utf8'));
|
||||
|
||||
fs.writeFileSync(path.join(fixture.sourceRoot, 'commands', 'plan.md'), 'upgraded payload\n');
|
||||
const upgradedSha = commit(fixture.sourceRoot, 'upgraded payload');
|
||||
const marketplacePath = path.join(fixture.sourceRoot, '.grok-plugin', 'marketplace.json');
|
||||
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, 'utf8'));
|
||||
marketplace.plugins[0].source.sha = upgradedSha;
|
||||
writeJson(marketplacePath, marketplace);
|
||||
commit(fixture.sourceRoot, 'upgraded catalog');
|
||||
const upgradePlan = createPlan(fixture, { hooks: false, mcp: [] });
|
||||
applyInstallPlan(upgradePlan);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(path.join(installedRoot(fixture), 'commands', 'plan.md'), 'utf8'),
|
||||
'upgraded payload\n'
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.join(installedRoot(fixture), 'commands', 'plan.md'), 'broken\n');
|
||||
const repair = repairInstalledStates({
|
||||
repoRoot: fixture.sourceRoot,
|
||||
projectRoot: fixture.sourceRoot,
|
||||
homeDir: fixture.homeDir,
|
||||
targets: ['grok'],
|
||||
env: {},
|
||||
});
|
||||
assert.strictEqual(repair.results[0].status, 'repaired', JSON.stringify(repair.results[0]));
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(path.join(installedRoot(fixture), 'commands', 'plan.md'), 'utf8'),
|
||||
'upgraded payload\n'
|
||||
);
|
||||
|
||||
const rollbackPlan = createPlan(
|
||||
fixture,
|
||||
{ hooks: false, mcp: [] },
|
||||
{ sourceSha: firstState.source.repoCommit }
|
||||
);
|
||||
applyInstallPlan(rollbackPlan);
|
||||
const rolledBackState = JSON.parse(fs.readFileSync(rollbackPlan.installStatePath, 'utf8'));
|
||||
assert.strictEqual(rolledBackState.source.repoCommit, firstState.source.repoCommit);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(path.join(installedRoot(fixture), 'commands', 'plan.md'), 'utf8'),
|
||||
'pinned payload\n'
|
||||
);
|
||||
|
||||
const uninstall = uninstallInstalledStates({
|
||||
repoRoot: fixture.sourceRoot,
|
||||
projectRoot: fixture.sourceRoot,
|
||||
homeDir: fixture.homeDir,
|
||||
targets: ['grok'],
|
||||
env: {},
|
||||
});
|
||||
assert.strictEqual(uninstall.results[0].status, 'uninstalled');
|
||||
assert.ok(!fs.existsSync(firstPlan.installStatePath));
|
||||
assert.ok(!fs.existsSync(installedRoot(fixture)));
|
||||
} finally {
|
||||
fs.rmSync(fixture.parent, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
@@ -1,8 +1,5 @@
|
||||
/**
|
||||
* Conformance tests for the Grok harness adapter.
|
||||
*
|
||||
* These tests require the shipped adapter (`scripts/lib/grok-harness-adapter.js`)
|
||||
* and the shared resolver. They do not reimplement adapter logic.
|
||||
* Read-only Grok harness boundary tests.
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
@@ -11,33 +8,26 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const adapter = require('../../scripts/lib/grok-harness-adapter');
|
||||
const { uninstallInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
||||
const {
|
||||
resolveEccRoot,
|
||||
pluginRootFromEnv,
|
||||
PLUGIN_ROOT_ENV_KEYS,
|
||||
SHA_PATTERN,
|
||||
readPinnedMarketplaceSource,
|
||||
} = require('../../scripts/lib/grok-source-identity');
|
||||
const {
|
||||
INLINE_RESOLVE,
|
||||
resolveEccRoot,
|
||||
} = require('../../scripts/lib/resolve-ecc-root');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const marketplacePath = path.join(repoRoot, '.grok-plugin', 'marketplace.json');
|
||||
const mcpPath = path.join(repoRoot, '.mcp.json');
|
||||
const ECC_SKILL_SENTINEL = path.join('skills', 'continuous-learning-v2');
|
||||
|
||||
const FIXTURE_SHA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||
const OTHER_SHA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
const TASTEFORGE_SHA = 'd8409a4b0813771235555e32e3d8046a73988bfa';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
@@ -47,128 +37,47 @@ function createTempDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-grok-adapter-'));
|
||||
}
|
||||
|
||||
function writeCompleteRoot(root) {
|
||||
function writeCompleteRoot(root, sha = null) {
|
||||
fs.mkdirSync(path.join(root, 'scripts', 'lib'), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, 'scripts', 'lib', 'utils.js'), '// stub\n');
|
||||
fs.mkdirSync(path.join(root, ECC_SKILL_SENTINEL), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'hooks'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'hooks', 'hooks.json'),
|
||||
JSON.stringify({
|
||||
hooks: {
|
||||
SessionStart: [{
|
||||
command: 'node -e "var e=process.env.PLUGIN_ROOT||process.env.CLAUDE_PLUGIN_ROOT||process.env.ECC_PLUGIN_ROOT"',
|
||||
}],
|
||||
},
|
||||
})
|
||||
);
|
||||
return root;
|
||||
}
|
||||
|
||||
function sourceFixture(root, sha = FIXTURE_SHA) {
|
||||
writeCompleteRoot(root);
|
||||
fs.writeFileSync(path.join(root, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: {
|
||||
'chrome-devtools': { command: 'npx', args: ['-y', 'chrome-devtools-mcp@latest'] },
|
||||
'other-mcp': { command: 'echo' },
|
||||
},
|
||||
}));
|
||||
return {
|
||||
sourceRoot: root,
|
||||
source: {
|
||||
source: 'url',
|
||||
url: 'https://github.com/affaan-m/ECC.git',
|
||||
sha,
|
||||
},
|
||||
mcpConfig: JSON.parse(fs.readFileSync(path.join(root, '.mcp.json'), 'utf8')),
|
||||
version: '2.2.0',
|
||||
};
|
||||
}
|
||||
|
||||
function gitCommit(sourceRoot, message) {
|
||||
const { execFileSync } = require('child_process');
|
||||
execFileSync('git', ['add', '.'], { cwd: sourceRoot, stdio: 'ignore' });
|
||||
execFileSync('git', [
|
||||
'-c', 'user.email=grok-adapter@test',
|
||||
'-c', 'user.name=GrokAdapter',
|
||||
'commit', '-m', message,
|
||||
], { cwd: sourceRoot, stdio: 'ignore' });
|
||||
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: sourceRoot, encoding: 'utf8' }).trim();
|
||||
}
|
||||
|
||||
function gitSourceFixture(sourceRoot) {
|
||||
const { execFileSync } = require('child_process');
|
||||
const fixture = sourceFixture(sourceRoot, FIXTURE_SHA);
|
||||
fs.mkdirSync(path.join(sourceRoot, '.grok-plugin'), { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, '.grok-plugin', 'marketplace.json'), JSON.stringify({
|
||||
name: 'ecc',
|
||||
plugins: [{
|
||||
name: 'ecc',
|
||||
version: '2.2.0',
|
||||
source: {
|
||||
source: 'url',
|
||||
url: 'https://github.com/affaan-m/ECC.git',
|
||||
sha: OTHER_SHA,
|
||||
},
|
||||
}],
|
||||
}));
|
||||
execFileSync('git', ['init'], { cwd: sourceRoot, stdio: 'ignore' });
|
||||
fixture.source.sha = gitCommit(sourceRoot, 'fixture');
|
||||
return fixture;
|
||||
fs.mkdirSync(path.join(root, 'skills', 'continuous-learning-v2'), { recursive: true });
|
||||
if (sha) fs.writeFileSync(path.join(root, '.ecc-source.json'), JSON.stringify({ sha }));
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Grok harness adapter ===\n');
|
||||
console.log('\n=== Grok harness adapter boundary ===\n');
|
||||
|
||||
if (test('shared PLUGIN_ROOT_ENV_KEYS does not include GROK_PLUGIN_ROOT', () => {
|
||||
assert.ok(Array.isArray(PLUGIN_ROOT_ENV_KEYS));
|
||||
assert.ok(!PLUGIN_ROOT_ENV_KEYS.includes('GROK_PLUGIN_ROOT'));
|
||||
if (test('shared root resolution has no Grok aliases or layout policy', () => {
|
||||
assert.ok(!INLINE_RESOLVE.includes('GROK_PLUGIN_ROOT'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('shared pluginRootFromEnv ignores GROK_PLUGIN_ROOT', () => {
|
||||
assert.strictEqual(pluginRootFromEnv({
|
||||
GROK_PLUGIN_ROOT: '/grok/root',
|
||||
ECC_PLUGIN_ROOT: '/ecc/root',
|
||||
}), '/ecc/root');
|
||||
assert.strictEqual(pluginRootFromEnv({ GROK_PLUGIN_ROOT: '/grok/root' }), '');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('shared resolveEccRoot ignores GROK_PLUGIN_ROOT and ~/.grok layout', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const grokRoot = path.join(homeDir, '.grok', 'installed-plugins', 'ecc-deadbeef');
|
||||
writeCompleteRoot(grokRoot);
|
||||
const previous = process.env.GROK_PLUGIN_ROOT;
|
||||
const previousClaude = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
const previousPlugin = process.env.PLUGIN_ROOT;
|
||||
const previousEcc = process.env.ECC_PLUGIN_ROOT;
|
||||
try {
|
||||
process.env.GROK_PLUGIN_ROOT = grokRoot;
|
||||
delete process.env.CLAUDE_PLUGIN_ROOT;
|
||||
delete process.env.PLUGIN_ROOT;
|
||||
delete process.env.ECC_PLUGIN_ROOT;
|
||||
const result = resolveEccRoot({ homeDir, envRoot: '' });
|
||||
assert.strictEqual(result, path.join(homeDir, '.claude'));
|
||||
assert.notStrictEqual(result, grokRoot);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.GROK_PLUGIN_ROOT;
|
||||
else process.env.GROK_PLUGIN_ROOT = previous;
|
||||
if (previousClaude === undefined) delete process.env.CLAUDE_PLUGIN_ROOT;
|
||||
else process.env.CLAUDE_PLUGIN_ROOT = previousClaude;
|
||||
if (previousPlugin === undefined) delete process.env.PLUGIN_ROOT;
|
||||
else process.env.PLUGIN_ROOT = previousPlugin;
|
||||
if (previousEcc === undefined) delete process.env.ECC_PLUGIN_ROOT;
|
||||
else process.env.ECC_PLUGIN_ROOT = previousEcc;
|
||||
}
|
||||
const root = path.join(homeDir, '.grok', 'installed-plugins', 'ecc-deadbeef');
|
||||
writeCompleteRoot(root);
|
||||
assert.strictEqual(resolveEccRoot({ homeDir, envRoot: '' }), path.join(homeDir, '.claude'));
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('shared hook runners do not require the Grok adapter', () => {
|
||||
if (test('GROK_PLUGIN_ROOT is translated only at the adapter boundary', () => {
|
||||
const mapped = adapter.toSharedPluginEnv({
|
||||
GROK_PLUGIN_ROOT: '/grok/root',
|
||||
PATH: '/bin',
|
||||
});
|
||||
assert.strictEqual(mapped.PLUGIN_ROOT, '/grok/root');
|
||||
assert.strictEqual(mapped.GROK_PLUGIN_ROOT, undefined);
|
||||
const existing = adapter.toSharedPluginEnv({
|
||||
GROK_PLUGIN_ROOT: '/grok/root',
|
||||
CLAUDE_PLUGIN_ROOT: '/claude/root',
|
||||
});
|
||||
assert.strictEqual(existing.PLUGIN_ROOT, undefined);
|
||||
assert.strictEqual(resolveEccRoot({ envRoot: existing.CLAUDE_PLUGIN_ROOT }), '/claude/root');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('shared hook and resolver files contain no Grok provider policy', () => {
|
||||
const files = [
|
||||
'scripts/lib/hook-flags.js',
|
||||
'scripts/lib/resolve-ecc-root.js',
|
||||
'scripts/hooks/plugin-hook-bootstrap.js',
|
||||
'scripts/hooks/run-with-flags.js',
|
||||
'scripts/hooks/observe-runner.js',
|
||||
@@ -176,670 +85,55 @@ function runTests() {
|
||||
];
|
||||
for (const relative of files) {
|
||||
const source = fs.readFileSync(path.join(repoRoot, relative), 'utf8');
|
||||
assert.ok(!source.includes('grok-harness-adapter'), `${relative} must not require the Grok adapter`);
|
||||
assert.ok(!source.includes('GROK_PLUGIN_ROOT'), `${relative} must not select GROK_PLUGIN_ROOT`);
|
||||
}
|
||||
const resolveSource = fs.readFileSync(path.join(repoRoot, 'scripts/lib/resolve-ecc-root.js'), 'utf8');
|
||||
assert.ok(!resolveSource.includes('GROK_PLUGIN_ROOT'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('toSharedPluginEnv maps GROK_PLUGIN_ROOT onto PLUGIN_ROOT and drops the Grok alias', () => {
|
||||
const mapped = adapter.toSharedPluginEnv({
|
||||
GROK_PLUGIN_ROOT: '/grok/root',
|
||||
PATH: '/bin',
|
||||
});
|
||||
assert.strictEqual(mapped.PLUGIN_ROOT, '/grok/root');
|
||||
assert.strictEqual(mapped.GROK_PLUGIN_ROOT, undefined);
|
||||
assert.strictEqual(pluginRootFromEnv(mapped), '/grok/root');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('toSharedPluginEnv does not let GROK override an existing Claude/Codex root', () => {
|
||||
const mapped = adapter.toSharedPluginEnv({
|
||||
CLAUDE_PLUGIN_ROOT: '/claude/root',
|
||||
GROK_PLUGIN_ROOT: '/grok/root',
|
||||
});
|
||||
assert.strictEqual(mapped.GROK_PLUGIN_ROOT, undefined);
|
||||
assert.strictEqual(pluginRootFromEnv(mapped), '/claude/root');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('adapter resolves GROK_PLUGIN_ROOT and ~/.grok installed-plugins', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
assert.strictEqual(
|
||||
adapter.resolveGrokPluginRoot({ env: { GROK_PLUGIN_ROOT: ' /from/grok ' }, homeDir }),
|
||||
'/from/grok'
|
||||
);
|
||||
const installed = path.join(homeDir, '.grok', 'installed-plugins', 'ecc-ab12cd34');
|
||||
writeCompleteRoot(installed);
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ env: {}, homeDir }), installed);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
assert.ok(!source.includes('GROK_PLUGIN_ROOT'), relative);
|
||||
assert.ok(!source.includes('grok-harness-adapter'), relative);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('runGrokInstall dry-run is the ECC consent path and denies Chrome DevTools by default', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const result = adapter.runGrokInstall({ dryRun: true, homeDir, repoRoot });
|
||||
assert.strictEqual(result.kind, 'grok-install-result');
|
||||
assert.strictEqual(result.dryRun, true);
|
||||
assert.strictEqual(result.plan.attachChromeDevtools, false);
|
||||
assert.strictEqual(result.plan.hooksEnabled, false);
|
||||
assert.strictEqual(result.nativeMcpOptedOut, true);
|
||||
assert.strictEqual(result.nativeHooksOptedOut, true);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('preview exposes canonical operations and their concrete destinations', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const plan = adapter.previewInstall({
|
||||
homeDir,
|
||||
source: { source: 'url', url: 'https://github.com/affaan-m/ECC.git', sha: FIXTURE_SHA },
|
||||
trust: true,
|
||||
consent: { hooks: true, mcp: { 'other-mcp': true } },
|
||||
mcpConfig: { mcpServers: { 'other-mcp': { command: 'echo' } } },
|
||||
});
|
||||
assert.ok(Array.isArray(plan.operations) && plan.operations.length > 0);
|
||||
assert.ok(plan.operations.every((operation) => (
|
||||
['copy-file', 'render-template', 'merge-json', 'remove'].includes(operation.kind)
|
||||
)));
|
||||
assert.ok(plan.operations.every((operation) => path.isAbsolute(operation.destinationPath)));
|
||||
assert.ok(plan.operations.some((operation) => operation.destinationPath.includes(path.join('.grok', 'plugins'))));
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('applyInstall copies the pinned git archive, not HEAD or untracked files', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
const fixture = gitSourceFixture(sourceRoot);
|
||||
const pinnedSha = fixture.source.sha;
|
||||
fs.writeFileSync(path.join(sourceRoot, 'untracked-extra.txt'), 'should-not-copy\n');
|
||||
fs.writeFileSync(path.join(sourceRoot, 'later-commit.txt'), 'later\n');
|
||||
gitCommit(sourceRoot, 'later');
|
||||
const plan = adapter.previewInstall({
|
||||
...fixture,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true },
|
||||
});
|
||||
const receipt = adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig });
|
||||
assert.ok(!fs.existsSync(path.join(receipt.installedRoot, 'untracked-extra.txt')));
|
||||
assert.ok(!fs.existsSync(path.join(receipt.installedRoot, 'later-commit.txt')));
|
||||
assert.ok(fs.existsSync(path.join(receipt.installedRoot, 'scripts', 'lib', 'utils.js')));
|
||||
const identity = JSON.parse(fs.readFileSync(
|
||||
path.join(receipt.installedRoot, adapter.SOURCE_IDENTITY_FILE),
|
||||
'utf8'
|
||||
));
|
||||
assert.strictEqual(identity.sha, pinnedSha);
|
||||
const installedCatalog = JSON.parse(fs.readFileSync(
|
||||
path.join(receipt.installedRoot, '.grok-plugin', 'marketplace.json'),
|
||||
'utf8'
|
||||
));
|
||||
assert.strictEqual(installedCatalog.plugins[0].source.sha, pinnedSha);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('applyInstall rejects a git pin that is not in the source repository', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
const fixture = gitSourceFixture(sourceRoot);
|
||||
fixture.source.sha = OTHER_SHA;
|
||||
const plan = adapter.previewInstall({
|
||||
...fixture,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true },
|
||||
});
|
||||
assert.throws(
|
||||
() => adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig }),
|
||||
/not in source git/
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('applyInstall rejects a non-Git source tree with only self-asserted identity', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
const fixture = sourceFixture(sourceRoot);
|
||||
fs.mkdirSync(path.join(sourceRoot, '.grok-plugin'), { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, '.grok-plugin', 'marketplace.json'), JSON.stringify({
|
||||
plugins: [{ name: 'ecc', version: fixture.version, source: fixture.source }],
|
||||
}));
|
||||
const plan = adapter.previewInstall({ ...fixture, homeDir, trust: true });
|
||||
assert.throws(
|
||||
() => adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig }),
|
||||
/unverifiable|verified source|source identity/i
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('runGrokInstall rejects malformed .mcp.json instead of silently disabling capabilities', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
fs.mkdirSync(path.join(sourceRoot, '.grok-plugin'), { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, '.grok-plugin', 'marketplace.json'), JSON.stringify({
|
||||
plugins: [{
|
||||
name: 'ecc',
|
||||
version: '2.2.0',
|
||||
source: { source: 'url', url: 'https://github.com/affaan-m/ECC.git', sha: FIXTURE_SHA },
|
||||
}],
|
||||
}));
|
||||
fs.writeFileSync(path.join(sourceRoot, '.grok-plugin', 'plugin.json'), JSON.stringify({
|
||||
name: 'ecc',
|
||||
hooks: '',
|
||||
mcpServers: '',
|
||||
}));
|
||||
fs.writeFileSync(path.join(sourceRoot, '.mcp.json'), '{not-json');
|
||||
assert.throws(
|
||||
() => adapter.runGrokInstall({ dryRun: true, homeDir, repoRoot: sourceRoot }),
|
||||
/\.mcp\.json|JSON/i
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('missing Grok root returns null', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ env: {}, homeDir }), null);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('in-repo marketplace source is a pinned Git URL', () => {
|
||||
const { execFileSync } = require('child_process');
|
||||
const plugin = adapter.readMarketplaceSource(marketplacePath);
|
||||
assert.strictEqual(plugin.source.url, 'https://github.com/affaan-m/ECC.git');
|
||||
assert.match(plugin.source.sha, adapter.SHA_PATTERN);
|
||||
assert.notStrictEqual(plugin.source.sha, TASTEFORGE_SHA);
|
||||
const tracked = execFileSync('git', ['ls-files', 'scripts/lib/grok-harness-adapter.js'], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
if (tracked) {
|
||||
execFileSync('git', ['show', `${plugin.source.sha}:scripts/lib/grok-harness-adapter.js`], {
|
||||
cwd: repoRoot,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
}
|
||||
assert.ok(adapter.sourceIdentity(plugin.source).includes(plugin.source.sha));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('native Grok plugin manifest opts trusted installs out of chrome-devtools', () => {
|
||||
const pluginJsonPath = path.join(repoRoot, '.grok-plugin', 'plugin.json');
|
||||
const manifest = adapter.readGrokPluginManifest(pluginJsonPath);
|
||||
assert.strictEqual(manifest.mcpServers, '');
|
||||
if (test('native trusted manifest defaults deny hooks and root MCP', () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, '.grok-plugin', 'plugin.json'), 'utf8'));
|
||||
assert.strictEqual(manifest.hooks, '');
|
||||
assert.strictEqual(adapter.nativePluginMcpOptedOut(manifest), true);
|
||||
assert.deepStrictEqual(adapter.nativeTrustedMcpServerNames(manifest), []);
|
||||
assert.strictEqual(adapter.nativeTrustedInstallAttachesChromeDevtools(manifest), false);
|
||||
const rootMcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
||||
assert.ok(rootMcp.mcpServers['chrome-devtools'], 'Claude/Codex root .mcp.json may still list chrome-devtools');
|
||||
assert.strictEqual(manifest.mcpServers, '');
|
||||
const rootMcp = JSON.parse(fs.readFileSync(path.join(repoRoot, '.mcp.json'), 'utf8'));
|
||||
assert.ok(rootMcp.mcpServers['chrome-devtools']);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('unpinned marketplace source is rejected', () => {
|
||||
assert.throws(
|
||||
() => adapter.assertPinnedSource({ source: 'url', url: 'https://github.com/affaan-m/ECC.git' }),
|
||||
/40-character lowercase commit SHA/
|
||||
);
|
||||
if (test('marketplace source is the pinned whole repository', () => {
|
||||
const source = readPinnedMarketplaceSource(repoRoot);
|
||||
assert.strictEqual(source.url, 'https://github.com/affaan-m/ECC.git');
|
||||
assert.match(source.sha, SHA_PATTERN);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('preview names hooks and each MCP capability; trusted default denies Chrome DevTools', () => {
|
||||
const mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
||||
const plan = adapter.previewInstall({
|
||||
source: { source: 'url', url: 'https://github.com/affaan-m/ECC.git', sha: FIXTURE_SHA },
|
||||
trust: true,
|
||||
mcpConfig,
|
||||
});
|
||||
const ids = plan.capabilities.map((item) => item.id);
|
||||
assert.ok(ids.includes('hooks'));
|
||||
assert.ok(ids.includes('chrome-devtools'));
|
||||
assert.strictEqual(plan.hooksEnabled, false);
|
||||
assert.strictEqual(plan.attachChromeDevtools, false);
|
||||
assert.ok(!plan.mcpAttached.includes('chrome-devtools'));
|
||||
assert.ok(plan.capabilities.every((item) => item.consented === false));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('untrusted install cannot enable hooks or MCP even with consent', () => {
|
||||
const plan = adapter.previewInstall({
|
||||
source: { source: 'url', url: 'https://github.com/affaan-m/ECC.git', sha: FIXTURE_SHA },
|
||||
trust: false,
|
||||
consent: { hooks: true, mcp: { 'chrome-devtools': true } },
|
||||
mcpConfig: { mcpServers: { 'chrome-devtools': { command: 'npx' } } },
|
||||
});
|
||||
assert.strictEqual(plan.trust, false);
|
||||
assert.strictEqual(plan.hooksEnabled, false);
|
||||
assert.strictEqual(plan.attachChromeDevtools, false);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('capability denial keeps Chrome DevTools out of a trusted receipt', () => {
|
||||
if (test('missing, disabled, native, cached, and pinned roots resolve deterministically', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
const sha = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||
try {
|
||||
const fixture = sourceFixture(sourceRoot);
|
||||
const plan = adapter.previewInstall({
|
||||
...fixture,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true, mcp: { 'chrome-devtools': false, 'other-mcp': true } },
|
||||
});
|
||||
assert.strictEqual(plan.hooksEnabled, true);
|
||||
assert.strictEqual(plan.attachChromeDevtools, false);
|
||||
assert.deepStrictEqual(plan.mcpAttached, ['other-mcp']);
|
||||
const receipt = adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig });
|
||||
const installedMcp = JSON.parse(fs.readFileSync(path.join(receipt.installedRoot, '.mcp.json'), 'utf8'));
|
||||
assert.ok(!installedMcp.mcpServers['chrome-devtools']);
|
||||
assert.ok(installedMcp.mcpServers['other-mcp']);
|
||||
assert.ok(receipt.hooksEnabled);
|
||||
assert.ok(!receipt.mcpAttached.includes('chrome-devtools'));
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ homeDir, env: {} }), null);
|
||||
const nativeRoot = path.join(homeDir, '.grok', 'plugins', 'ecc');
|
||||
writeCompleteRoot(nativeRoot);
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ homeDir, env: {} }), nativeRoot);
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ homeDir, enabled: false }), null);
|
||||
|
||||
if (test('trusted install without MCP consent does not attach root chrome-devtools', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
const fixture = sourceFixture(sourceRoot);
|
||||
const plan = adapter.previewInstall({
|
||||
...fixture,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true },
|
||||
});
|
||||
const receipt = adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig });
|
||||
const installedMcp = JSON.parse(fs.readFileSync(path.join(receipt.installedRoot, '.mcp.json'), 'utf8'));
|
||||
assert.deepStrictEqual(installedMcp.mcpServers, {});
|
||||
assert.strictEqual(receipt.attachChromeDevtools, undefined);
|
||||
assert.deepStrictEqual(receipt.mcpAttached, []);
|
||||
const statePath = adapter.grokInstallStatePath(homeDir);
|
||||
assert.ok(fs.existsSync(statePath));
|
||||
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
assert.strictEqual(state.schemaVersion, 'ecc.install.v1');
|
||||
assert.strictEqual(state.target.id, 'grok-home');
|
||||
assert.strictEqual(state.target.target, 'grok');
|
||||
assert.strictEqual(state.source.repoCommit, fixture.source.sha);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.grok', 'ecc-adapter')));
|
||||
const mapped = adapter.toSharedPluginEnv({ GROK_PLUGIN_ROOT: receipt.installedRoot });
|
||||
assert.strictEqual(mapped.PLUGIN_ROOT, receipt.installedRoot);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('applyInstall maps GROK_PLUGIN_ROOT at the installed hook boundary when hooks are consented', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
const fixture = sourceFixture(sourceRoot);
|
||||
const plan = adapter.previewInstall({
|
||||
...fixture,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true },
|
||||
});
|
||||
const receipt = adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig });
|
||||
assert.strictEqual(receipt.trust, true);
|
||||
const destHooks = fs.readFileSync(path.join(receipt.installedRoot, 'hooks', 'hooks.json'), 'utf8');
|
||||
assert.ok(destHooks.includes('GROK_PLUGIN_ROOT'));
|
||||
const sharedEnv = adapter.readInstalledSharedEnv(receipt.installedRoot);
|
||||
assert.strictEqual(sharedEnv.PLUGIN_ROOT, receipt.installedRoot);
|
||||
const repoHooks = fs.readFileSync(path.join(repoRoot, 'hooks', 'hooks.json'), 'utf8');
|
||||
assert.ok(!repoHooks.includes('GROK_PLUGIN_ROOT'));
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('applyInstall rejects a source tree whose marketplace pin does not match the plan', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
const fixture = sourceFixture(sourceRoot);
|
||||
fs.mkdirSync(path.join(sourceRoot, '.grok-plugin'), { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, '.grok-plugin', 'marketplace.json'), JSON.stringify({
|
||||
name: 'ecc',
|
||||
plugins: [{
|
||||
name: 'ecc',
|
||||
version: '2.2.0',
|
||||
source: {
|
||||
source: 'url',
|
||||
url: 'https://github.com/affaan-m/ECC.git',
|
||||
sha: OTHER_SHA,
|
||||
},
|
||||
}],
|
||||
}));
|
||||
const plan = adapter.previewInstall({
|
||||
...fixture,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true },
|
||||
});
|
||||
assert.throws(
|
||||
() => adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig }),
|
||||
/marketplace sha does not match/
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('install is root-contained; Windows-style escape is rejected', () => {
|
||||
const grokRoot = 'C:\\Users\\ecc\\.grok';
|
||||
assert.ok(adapter.isContained('C:\\Users\\ecc\\.grok\\installed-plugins\\ecc-aaa', grokRoot, path.win32));
|
||||
assert.ok(!adapter.isContained('C:\\Windows\\System32', grokRoot, path.win32));
|
||||
assert.ok(!adapter.isContained('C:\\Users\\ecc\\.grok\\..\\..\\Windows', grokRoot, path.win32));
|
||||
assert.throws(
|
||||
() => adapter.assertRootContainment('C:\\Windows\\System32', grokRoot, path.win32),
|
||||
/escapes Grok root/
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('applyInstall rejects a symlinked plugin-directory ancestor without writing outside Grok home', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
const outsideRoot = createTempDir();
|
||||
try {
|
||||
const fixture = gitSourceFixture(sourceRoot);
|
||||
const grokRoot = path.join(homeDir, '.grok');
|
||||
fs.mkdirSync(grokRoot, { recursive: true });
|
||||
try {
|
||||
fs.symlinkSync(outsideRoot, path.join(grokRoot, 'plugins'), 'dir');
|
||||
} catch {
|
||||
console.log(' (directory symlink unsupported on this platform; skipping)');
|
||||
return;
|
||||
}
|
||||
const plan = adapter.previewInstall({ ...fixture, homeDir, trust: true });
|
||||
assert.throws(() => adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig }), /outside|symlink|root/i);
|
||||
assert.deepStrictEqual(fs.readdirSync(outsideRoot), []);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
fs.rmSync(outsideRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('applyInstall rejects a final destination symlink without modifying its target', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
const outsideRoot = createTempDir();
|
||||
try {
|
||||
const fixture = gitSourceFixture(sourceRoot);
|
||||
const plan = adapter.previewInstall({ ...fixture, homeDir, trust: true });
|
||||
const destination = adapter.installDestination(homeDir, plan);
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
fs.writeFileSync(path.join(outsideRoot, 'sentinel.txt'), 'unchanged\n');
|
||||
try {
|
||||
fs.symlinkSync(outsideRoot, destination, 'dir');
|
||||
} catch {
|
||||
console.log(' (directory symlink unsupported on this platform; skipping)');
|
||||
return;
|
||||
}
|
||||
assert.throws(() => adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig }), /outside|symlink|root/i);
|
||||
assert.deepStrictEqual(fs.readdirSync(outsideRoot), ['sentinel.txt']);
|
||||
assert.strictEqual(fs.readFileSync(path.join(outsideRoot, 'sentinel.txt'), 'utf8'), 'unchanged\n');
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
fs.rmSync(outsideRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('canonical lifecycle uninstalls a Grok receipt with only supported operation kinds', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
const projectRoot = createTempDir();
|
||||
try {
|
||||
const fixture = gitSourceFixture(sourceRoot);
|
||||
const plan = adapter.previewInstall({ ...fixture, homeDir, trust: true, consent: { hooks: true } });
|
||||
const receipt = adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig });
|
||||
const state = JSON.parse(fs.readFileSync(adapter.grokInstallStatePath(homeDir), 'utf8'));
|
||||
assert.ok(state.operations.every((operation) => (
|
||||
['copy-file', 'render-template', 'merge-json', 'remove'].includes(operation.kind)
|
||||
)));
|
||||
const result = uninstallInstalledStates({
|
||||
repoRoot,
|
||||
homeDir,
|
||||
projectRoot,
|
||||
targets: ['grok'],
|
||||
});
|
||||
assert.strictEqual(result.results[0].status, 'uninstalled');
|
||||
assert.ok(!fs.existsSync(receipt.installedRoot));
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
fs.rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('consented capabilities are installed in Grok native discovery and denied capabilities are absent', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
const fixture = gitSourceFixture(sourceRoot);
|
||||
const plan = adapter.previewInstall({
|
||||
...fixture,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true, mcp: { 'other-mcp': true, 'chrome-devtools': false } },
|
||||
});
|
||||
const receipt = adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig });
|
||||
assert.ok(receipt.installedRoot.startsWith(path.join(homeDir, '.grok', 'plugins') + path.sep));
|
||||
assert.ok(fs.existsSync(path.join(receipt.installedRoot, 'hooks', 'hooks.json')));
|
||||
const installedManifest = JSON.parse(fs.readFileSync(
|
||||
path.join(receipt.installedRoot, '.grok-plugin', 'plugin.json'),
|
||||
'utf8'
|
||||
));
|
||||
assert.notStrictEqual(installedManifest.hooks, '');
|
||||
assert.notStrictEqual(installedManifest.mcpServers, '');
|
||||
const installedMcp = JSON.parse(fs.readFileSync(path.join(receipt.installedRoot, '.mcp.json'), 'utf8'));
|
||||
assert.deepStrictEqual(Object.keys(installedMcp.mcpServers), ['other-mcp']);
|
||||
assert.ok(!JSON.stringify(installedMcp).includes('chrome-devtools'));
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('upgrade, uninstall, and rollback are receipt-backed on fixture trees', () => {
|
||||
const homeDir = createTempDir();
|
||||
const firstRoot = createTempDir();
|
||||
const secondRoot = createTempDir();
|
||||
try {
|
||||
const first = sourceFixture(firstRoot, FIXTURE_SHA);
|
||||
const second = sourceFixture(secondRoot, OTHER_SHA);
|
||||
const firstPlan = adapter.previewInstall({
|
||||
...first,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true },
|
||||
});
|
||||
const firstReceipt = adapter.applyInstall(firstPlan, { mcpConfig: first.mcpConfig, now: '2026-09-03T00:00:00.000Z' });
|
||||
assert.strictEqual(firstReceipt.operation, 'install');
|
||||
assert.ok(fs.existsSync(firstReceipt.installedRoot));
|
||||
|
||||
const secondPlan = adapter.previewInstall({
|
||||
...second,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true, mcp: { 'chrome-devtools': true } },
|
||||
});
|
||||
const upgraded = adapter.applyInstall(secondPlan, { mcpConfig: second.mcpConfig, now: '2026-09-03T01:00:00.000Z' });
|
||||
assert.strictEqual(upgraded.operation, 'upgrade');
|
||||
assert.strictEqual(upgraded.previousReceiptId, firstReceipt.id);
|
||||
assert.notStrictEqual(upgraded.installedRoot, firstReceipt.installedRoot);
|
||||
assert.ok(upgraded.mcpAttached.includes('chrome-devtools'));
|
||||
assert.ok(fs.existsSync(firstReceipt.installedRoot));
|
||||
assert.ok(fs.existsSync(upgraded.installedRoot));
|
||||
assert.strictEqual(adapter.listManagedInstallRoots(homeDir).length, 2);
|
||||
|
||||
const rolled = adapter.rollback(homeDir);
|
||||
assert.strictEqual(rolled.operation, 'rollback');
|
||||
assert.strictEqual(rolled.id, firstReceipt.id);
|
||||
assert.strictEqual(adapter.loadCurrentReceipt(homeDir).id, firstReceipt.id);
|
||||
assert.deepStrictEqual(adapter.loadCurrentReceipt(homeDir).mcpAttached, []);
|
||||
assert.ok(fs.existsSync(firstReceipt.installedRoot));
|
||||
assert.ok(!fs.existsSync(upgraded.installedRoot));
|
||||
|
||||
const removed = adapter.uninstall(homeDir);
|
||||
assert.strictEqual(removed.operation, 'uninstall');
|
||||
assert.ok(!fs.existsSync(firstReceipt.installedRoot));
|
||||
assert.ok(!fs.existsSync(upgraded.installedRoot));
|
||||
assert.deepStrictEqual(adapter.listManagedInstallRoots(homeDir), []);
|
||||
assert.strictEqual(adapter.loadCurrentReceipt(homeDir), null);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(firstRoot, { recursive: true, force: true });
|
||||
fs.rmSync(secondRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('disabled plugin is not resolved as the active Grok root', () => {
|
||||
const homeDir = createTempDir();
|
||||
const sourceRoot = createTempDir();
|
||||
try {
|
||||
const fixture = sourceFixture(sourceRoot);
|
||||
const plan = adapter.previewInstall({
|
||||
...fixture,
|
||||
homeDir,
|
||||
trust: true,
|
||||
consent: { hooks: true },
|
||||
});
|
||||
const receipt = adapter.applyInstall(plan, { mcpConfig: fixture.mcpConfig });
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ homeDir }), receipt.installedRoot);
|
||||
adapter.setPluginEnabled(homeDir, false);
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ homeDir }), null);
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ homeDir, requireEnabled: false }), receipt.installedRoot);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
fs.rmSync(sourceRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('multiple cached versions select only the pinned SHA', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const cacheBase = path.join(homeDir, '.grok', 'plugins', 'cache', 'ecc', 'affaan-m');
|
||||
const oldRoot = path.join(cacheBase, '2.1.0');
|
||||
const newRoot = path.join(cacheBase, '2.2.0');
|
||||
writeCompleteRoot(oldRoot);
|
||||
writeCompleteRoot(newRoot);
|
||||
fs.writeFileSync(path.join(oldRoot, adapter.SOURCE_IDENTITY_FILE), JSON.stringify({
|
||||
source: 'url',
|
||||
url: 'https://github.com/affaan-m/ECC.git',
|
||||
sha: OTHER_SHA,
|
||||
}));
|
||||
fs.writeFileSync(path.join(newRoot, adapter.SOURCE_IDENTITY_FILE), JSON.stringify({
|
||||
source: 'url',
|
||||
url: 'https://github.com/affaan-m/ECC.git',
|
||||
sha: FIXTURE_SHA,
|
||||
}));
|
||||
const versions = adapter.listCachedGrokVersions(homeDir);
|
||||
assert.strictEqual(versions.length, 2);
|
||||
const selected = adapter.selectPinnedCachedVersion(versions, FIXTURE_SHA);
|
||||
assert.strictEqual(selected.installedRoot, newRoot);
|
||||
assert.strictEqual(
|
||||
adapter.resolveGrokPluginRoot({ homeDir, env: {}, pinnedSha: FIXTURE_SHA }),
|
||||
newRoot
|
||||
);
|
||||
assert.strictEqual(adapter.selectPinnedCachedVersion(versions, 'cccccccccccccccccccccccccccccccccccccccc'), null);
|
||||
const cachedRoot = path.join(homeDir, '.grok', 'plugins', 'cache', 'ecc', 'affaan-m', '2.2.1');
|
||||
writeCompleteRoot(cachedRoot, sha);
|
||||
assert.strictEqual(adapter.selectPinnedCachedVersion(adapter.listCachedGrokVersions(homeDir), sha).installedRoot, cachedRoot);
|
||||
assert.strictEqual(adapter.resolveGrokPluginRoot({ homeDir, pinnedSha: sha }), cachedRoot);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('Windows install destination stays under .grok\\installed-plugins', () => {
|
||||
const dest = adapter.installDestination(
|
||||
'C:\\Users\\ecc',
|
||||
{ pluginId: 'ecc', source: { sha: FIXTURE_SHA } },
|
||||
path.win32
|
||||
);
|
||||
if (test('Windows Grok roots stay adapter-owned', () => {
|
||||
assert.strictEqual(
|
||||
dest,
|
||||
path.win32.join('C:\\Users\\ecc', '.grok', 'installed-plugins', `ecc-${FIXTURE_SHA.slice(0, 12)}`)
|
||||
adapter.grokInstallStatePath('C:\\Users\\ecc', path.win32),
|
||||
path.win32.join('C:\\Users\\ecc', '.grok', 'ecc', 'install-state.json')
|
||||
);
|
||||
assert.ok(adapter.isContained(dest, 'C:\\Users\\ecc\\.grok', path.win32));
|
||||
assert.strictEqual(adapter.resolveHomeDir({ ECC_GROK_HOME: 'D:\\Users\\ecc' }), 'D:\\Users\\ecc');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('previewInstall and grok-home.planOperations use the same hook/MCP consent gate', () => {
|
||||
const grokHome = require('../../scripts/lib/install-targets/grok-home');
|
||||
const mcpConfig = {
|
||||
mcpServers: {
|
||||
'chrome-devtools': { command: 'npx' },
|
||||
'other-mcp': { command: 'echo' },
|
||||
},
|
||||
};
|
||||
const source = {
|
||||
source: 'url',
|
||||
url: 'https://github.com/affaan-m/ECC.git',
|
||||
sha: FIXTURE_SHA,
|
||||
};
|
||||
const modules = [
|
||||
{ id: 'hooks-runtime', paths: ['hooks', 'hooks/hooks.json'] },
|
||||
{ id: 'mcp', paths: ['.mcp.json', 'mcp-configs'] },
|
||||
{ id: 'skills', paths: ['skills'] },
|
||||
];
|
||||
const consent = { hooks: true, mcp: { 'other-mcp': true, 'chrome-devtools': false } };
|
||||
const plan = adapter.previewInstall({
|
||||
source,
|
||||
trust: true,
|
||||
consent,
|
||||
mcpConfig,
|
||||
});
|
||||
const operations = grokHome.planOperations({
|
||||
homeDir: '/Users/example',
|
||||
modules,
|
||||
trust: true,
|
||||
consent,
|
||||
});
|
||||
const opPaths = operations.map((operation) => String(operation.sourceRelativePath).replace(/\\/g, '/'));
|
||||
assert.strictEqual(plan.hooksEnabled, true);
|
||||
assert.deepStrictEqual(plan.mcpAttached, ['other-mcp']);
|
||||
assert.ok(opPaths.includes('hooks') || opPaths.includes('hooks/hooks.json'));
|
||||
assert.ok(!opPaths.some((item) => item === '.mcp.json' || item.startsWith('mcp-configs')));
|
||||
assert.ok(opPaths.includes('skills'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('native plugin.json opt-out is the grok plugin --trust contract', () => {
|
||||
const { spawnSync } = require('child_process');
|
||||
const pluginJsonPath = path.join(repoRoot, '.grok-plugin', 'plugin.json');
|
||||
const manifest = adapter.readGrokPluginManifest(pluginJsonPath);
|
||||
assert.strictEqual(adapter.nativeTrustedInstallAttachesChromeDevtools(manifest), false);
|
||||
assert.deepStrictEqual(adapter.nativeTrustedMcpServerNames(manifest), []);
|
||||
const validated = spawnSync('grok', ['plugin', 'validate', '.'], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
if (!validated.error) {
|
||||
assert.strictEqual(validated.status, 0, validated.stderr || validated.stdout);
|
||||
assert.ok(!/chrome-devtools/i.test(validated.stdout || ''));
|
||||
if (test('the adapter exports no private install lifecycle', () => {
|
||||
for (const name of ['previewInstall', 'applyInstall', 'uninstall', 'rollback', 'runGrokInstall']) {
|
||||
assert.strictEqual(adapter[name], undefined, name);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ function runTests() {
|
||||
adal: ['project', './.adal'],
|
||||
hermes: ['home', '~/.hermes'],
|
||||
openclaw: ['home', '~/.openclaw'],
|
||||
grok: ['native', '~/.grok'],
|
||||
};
|
||||
|
||||
for (const [id, [scopeId, root]] of Object.entries(expected)) {
|
||||
@@ -175,7 +176,7 @@ function runTests() {
|
||||
assert.throws(() => normalizeHarnessSelection('none'), /At least one guided harness/);
|
||||
assert.throws(() => normalizeHarnessSelection('all,codex'), /cannot be combined/i);
|
||||
assert.throws(() => normalizeHarnessSelection('cursor'), /advanced.*not guided-ready/i);
|
||||
assert.throws(() => normalizeHarnessSelection('grok'), /Unknown guided harness selection/);
|
||||
assert.throws(() => normalizeHarnessSelection('grok'), /advanced.*not guided-ready/i);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('exports deeply frozen records while list helpers return safe array copies', () => {
|
||||
|
||||
@@ -52,6 +52,23 @@ function runTests() {
|
||||
assert.deepStrictEqual(parsed.languages, []);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('parses and validates the Grok rollback source pin', () => {
|
||||
const sha = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
||||
const parsed = parseInstallArgs([
|
||||
'node',
|
||||
'scripts/install-apply.js',
|
||||
'--target', 'grok',
|
||||
'--profile', 'core',
|
||||
'--source-sha', sha,
|
||||
]);
|
||||
assert.strictEqual(parsed.sourceSha, sha);
|
||||
assert.strictEqual(normalizeInstallRequest(parsed).sourceSha, sha);
|
||||
assert.throws(
|
||||
() => normalizeInstallRequest({ ...parsed, target: 'claude' }),
|
||||
/requires --target grok/
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('parses --locale argument', () => {
|
||||
const parsed = parseInstallArgs([
|
||||
'node',
|
||||
|
||||
@@ -87,10 +87,46 @@ function runTests() {
|
||||
consent: { mcp: { 'chrome-devtools': true } },
|
||||
});
|
||||
const mcpPaths = mcpAllowed.map((operation) => String(operation.sourceRelativePath).replace(/\\/g, '/'));
|
||||
assert.ok(!mcpPaths.some((item) => item === '.mcp.json' || item.startsWith('mcp-configs')));
|
||||
assert.ok(mcpPaths.includes('.mcp.json'));
|
||||
assert.ok(mcpAllowed.some((operation) => (
|
||||
operation.contentTransform === 'grok-mcp-consent'
|
||||
&& operation.grokMcpIds.includes('chrome-devtools')
|
||||
)));
|
||||
assert.ok(!mcpPaths.some((item) => item === 'hooks' || item.startsWith('hooks/')));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('grok MCP consent rejects malformed configs and unknown capabilities', () => {
|
||||
const grok = getInstallTargetAdapter('grok');
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-grok-mcp-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(tempRoot, '.mcp.json'), '{"mcpServers": []}\n');
|
||||
assert.throws(
|
||||
() => grok.planOperations({
|
||||
homeDir: tempRoot,
|
||||
repoRoot: tempRoot,
|
||||
modules: [],
|
||||
trust: true,
|
||||
consent: { mcp: { 'unknown-mcp': true } },
|
||||
}),
|
||||
/mcpServers object/
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.join(tempRoot, '.mcp.json'), '{"mcpServers": {"known-mcp": {}}}\n');
|
||||
assert.throws(
|
||||
() => grok.planOperations({
|
||||
homeDir: tempRoot,
|
||||
repoRoot: tempRoot,
|
||||
modules: [],
|
||||
trust: true,
|
||||
consent: { mcp: { 'unknown-mcp': true } },
|
||||
}),
|
||||
/Unknown Grok MCP capability: unknown-mcp/
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('resolves cursor adapter root and install-state path from project root', () => {
|
||||
const adapter = getInstallTargetAdapter('cursor');
|
||||
const projectRoot = '/workspace/app';
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
* Tests for scripts/lib/resolve-ecc-root.js
|
||||
*
|
||||
* Covers the ECC root resolution fallback chain:
|
||||
* 1. GROK_PLUGIN_ROOT / CLAUDE_PLUGIN_ROOT / ECC_PLUGIN_ROOT env vars
|
||||
* 1. CLAUDE_PLUGIN_ROOT env var
|
||||
* 2. Standard install (~/.claude/)
|
||||
* 3. Exact legacy plugin roots under ~/.claude/plugins/
|
||||
* 4. Plugin cache auto-detection
|
||||
* 5. Grok plugin homes (~/.grok/plugins/, ~/.grok/installed-plugins/)
|
||||
* 6. Fallback to ~/.claude/
|
||||
* 5. Fallback to ~/.claude/
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
@@ -18,11 +17,7 @@ const CURRENT_PACKAGE_VERSION = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')
|
||||
).version;
|
||||
|
||||
const {
|
||||
resolveEccRoot,
|
||||
INLINE_RESOLVE,
|
||||
pluginRootFromEnv,
|
||||
} = require('../../scripts/lib/resolve-ecc-root');
|
||||
const { resolveEccRoot, INLINE_RESOLVE } = require('../../scripts/lib/resolve-ecc-root');
|
||||
|
||||
// Sentinel ECC skill that resolveEccRoot() requires (alongside the script tree)
|
||||
// before accepting a root for skill consumers. Kept in sync with the module's
|
||||
@@ -62,24 +57,6 @@ function setupLegacyPluginInstall(homeDir, segments) {
|
||||
fs.mkdirSync(path.join(legacyDir, ECC_SKILL_SENTINEL), { recursive: true });
|
||||
return legacyDir;
|
||||
}
|
||||
function setupVendorPluginInstall(homeDir, vendor, segments) {
|
||||
const pluginDir = path.join(homeDir, vendor, 'plugins', ...segments);
|
||||
const scriptDir = path.join(pluginDir, 'scripts', 'lib');
|
||||
fs.mkdirSync(scriptDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(scriptDir, 'utils.js'), '// stub');
|
||||
fs.mkdirSync(path.join(pluginDir, ECC_SKILL_SENTINEL), { recursive: true });
|
||||
return pluginDir;
|
||||
}
|
||||
|
||||
function setupGrokInstalledPlugin(homeDir, dirName) {
|
||||
const pluginDir = path.join(homeDir, '.grok', 'installed-plugins', dirName);
|
||||
const scriptDir = path.join(pluginDir, 'scripts', 'lib');
|
||||
fs.mkdirSync(scriptDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(scriptDir, 'utils.js'), '// stub');
|
||||
fs.mkdirSync(path.join(pluginDir, ECC_SKILL_SENTINEL), { recursive: true });
|
||||
return pluginDir;
|
||||
}
|
||||
|
||||
function setupPluginCache(homeDir, pluginSlug, orgName, version) {
|
||||
const cacheDir = path.join(
|
||||
homeDir, '.claude', 'plugins', 'cache',
|
||||
@@ -537,119 +514,6 @@ module.exports = { resolveEccRoot() { assert.strictEqual(process.env.HOME, ${JSO
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('pluginRootFromEnv prefers GROK_PLUGIN_ROOT then CLAUDE then ECC', () => {
|
||||
assert.strictEqual(
|
||||
pluginRootFromEnv({
|
||||
GROK_PLUGIN_ROOT: '/grok/root',
|
||||
CLAUDE_PLUGIN_ROOT: '/claude/root',
|
||||
ECC_PLUGIN_ROOT: '/ecc/root',
|
||||
}),
|
||||
'/grok/root'
|
||||
);
|
||||
assert.strictEqual(
|
||||
pluginRootFromEnv({
|
||||
CLAUDE_PLUGIN_ROOT: '/claude/root',
|
||||
ECC_PLUGIN_ROOT: '/ecc/root',
|
||||
}),
|
||||
'/claude/root'
|
||||
);
|
||||
assert.strictEqual(pluginRootFromEnv({ ECC_PLUGIN_ROOT: '/ecc/root' }), '/ecc/root');
|
||||
assert.strictEqual(pluginRootFromEnv({ GROK_PLUGIN_ROOT: ' /trimmed/grok ' }), '/trimmed/grok');
|
||||
assert.strictEqual(pluginRootFromEnv({ GROK_PLUGIN_ROOT: ' ' }), '');
|
||||
assert.strictEqual(pluginRootFromEnv({}), '');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('reads GROK_PLUGIN_ROOT from process.env when envRoot is omitted', () => {
|
||||
const previous = process.env.GROK_PLUGIN_ROOT;
|
||||
const previousClaude = process.env.CLAUDE_PLUGIN_ROOT;
|
||||
const previousEcc = process.env.ECC_PLUGIN_ROOT;
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
process.env.GROK_PLUGIN_ROOT = '/from/grok/env';
|
||||
delete process.env.CLAUDE_PLUGIN_ROOT;
|
||||
delete process.env.ECC_PLUGIN_ROOT;
|
||||
assert.strictEqual(resolveEccRoot({ homeDir }), '/from/grok/env');
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
if (previous === undefined) delete process.env.GROK_PLUGIN_ROOT;
|
||||
else process.env.GROK_PLUGIN_ROOT = previous;
|
||||
if (previousClaude === undefined) delete process.env.CLAUDE_PLUGIN_ROOT;
|
||||
else process.env.CLAUDE_PLUGIN_ROOT = previousClaude;
|
||||
if (previousEcc === undefined) delete process.env.ECC_PLUGIN_ROOT;
|
||||
else process.env.ECC_PLUGIN_ROOT = previousEcc;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('finds Grok plugin install at ~/.grok/plugins/ecc', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupVendorPluginInstall(homeDir, '.grok', ['ecc']);
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('finds Grok plugin install at ~/.grok/plugins/ecc@ecc', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupVendorPluginInstall(homeDir, '.grok', ['ecc@ecc']);
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('finds Grok marketplace plugin install at ~/.grok/plugins/marketplaces/ecc', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupVendorPluginInstall(homeDir, '.grok', ['marketplaces', 'ecc']);
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('finds Grok installed-plugins hash directory when that is the only complete root', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const expected = setupGrokInstalledPlugin(homeDir, 'ecc-ab12cd34');
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, expected);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('prefers a complete Claude root over a complete Grok root when env is unset', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
const claudeDir = setupStandardInstall(homeDir);
|
||||
setupGrokInstalledPlugin(homeDir, 'ecc-ab12cd34');
|
||||
const result = resolveEccRoot({ envRoot: '', homeDir });
|
||||
assert.strictEqual(result, claudeDir);
|
||||
} finally {
|
||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('INLINE_RESOLVE returns GROK_PLUGIN_ROOT when set', () => {
|
||||
const { execFileSync } = require('child_process');
|
||||
const env = { ...process.env, GROK_PLUGIN_ROOT: '/inline/grok/root' };
|
||||
delete env.CLAUDE_PLUGIN_ROOT;
|
||||
delete env.ECC_PLUGIN_ROOT;
|
||||
const result = execFileSync('node', [
|
||||
'-e', `console.log(${INLINE_RESOLVE})`,
|
||||
], {
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
assert.strictEqual(result, '/inline/grok/root');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('INLINE_RESOLVE falls back to ~/.claude/ when nothing found', () => {
|
||||
const homeDir = createTempDir();
|
||||
try {
|
||||
|
||||
@@ -733,7 +733,7 @@ test('grok plugin.json keeps Claude-incompatible fields out of the Grok manifest
|
||||
assert.strictEqual(
|
||||
grokPlugin.hooks,
|
||||
'',
|
||||
'Empty hooks string opts the native Grok CLI surface out of hooks/hooks.json; ECC hook consent is applyInstall'
|
||||
'Empty hooks string opts the native Grok CLI surface out of hooks/hooks.json; ECC hook consent is canonical install state'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -805,8 +805,8 @@ test('package.json files includes the Grok plugin directory', () => {
|
||||
|
||||
test('.grok-plugin README documents install, trust, enable, MCP, and hook profile', () => {
|
||||
const readme = fs.readFileSync(grokPluginReadmePath, 'utf8');
|
||||
assert.ok(readme.includes('previewInstall'), 'ECC trusted install is previewInstall');
|
||||
assert.ok(readme.includes('applyInstall'), 'ECC trusted install is applyInstall');
|
||||
assert.ok(readme.includes('canonical installer'), 'ECC trusted install uses the canonical installer');
|
||||
assert.ok(readme.includes('install-apply.js --target grok'), 'Grok entrypoint delegates to install-apply');
|
||||
assert.ok(readme.includes('grok-install.js'), 'Operator ECC consent path is scripts/grok-install.js');
|
||||
assert.ok(readme.includes('grok plugin marketplace add affaan-m/ECC'), 'Expected marketplace add for the canonical ECC repo');
|
||||
assert.ok(readme.includes('grok plugin enable ecc'), 'Grok plugins stay off until enabled');
|
||||
|
||||
Reference in new issue
Block a user