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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ Evolver integrates with major agent runtimes through `setup-hooks`. Run it once
| [opencode](https://opencode.ai) | `evolver setup-hooks --platform=opencode` | Plugin at `~/.opencode/plugins/evolver.js` + scripts in `~/.opencode/hooks/`. Restart opencode. |
| [OpenClaw](https://openclaw.com) | No setup needed | OpenClaw natively interprets the `sessions_spawn(...)` stdout directives Evolver emits. Just run `evolver` from inside an OpenClaw session. |

Desktop integrations and automation should pass an explicit config root instead
of relying on the caller's current working directory:

```bash
evolver setup-hooks --platform=claude-code --root=/absolute/workspace
evolver setup-hooks --platform=claude-code --root=/absolute/workspace --verify --json
```

`--runtime=<platform>` remains a compatibility alias for `--platform`. When
`--verify --json` is used, stdout contains exactly one JSON health report so a
host application can distinguish a configured integration from a working one.

#### Codex caveats

The Codex CLI exposes `SessionStart` / `Stop` / `PostToolUse` hooks (which is
Expand Down
11 changes: 11 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ evolver setup-hooks --platform=claude-code

通过 `~/.claude/` 向 Claude Code 的 hook 系统注册 Evolver。安装完成后重启 Claude Code CLI。

桌面应用或自动化调用时应显式传入配置根目录,不要依赖调用进程的当前目录:

```bash
evolver setup-hooks --platform=claude-code --root=/绝对路径/工作区
evolver setup-hooks --platform=claude-code --root=/绝对路径/工作区 --verify --json
```

`--runtime=<platform>` 作为 `--platform` 的兼容别名保留。使用
`--verify --json` 时,stdout 只输出一个机器可读的 JSON 健康报告,宿主可以据此区分
“配置已写入”和“接入确实可用”。

#### OpenClaw

OpenClaw 会识别 Evolver 向 stdout 输出的 `sessions_spawn(...)` 协议,**无需安装 hooks**。将 Evolver 克隆到 OpenClaw workspace 中,在会话内运行即可:
Expand Down
45 changes: 45 additions & 0 deletions cli-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,50 @@ const PROXY_PATH_FLAGS = new Map([
['--env-file', 'envFile'],
]);

function optionValue(argv, name) {
for (let index = 0; index < argv.length; index += 1) {
const arg = String(argv[index]);
if (arg === name) {
const value = argv[index + 1];
if (value === undefined || String(value).startsWith('-')) {
throw new Error(name + ' requires a value');
}
return String(value);
}
if (arg.startsWith(name + '=')) {
const value = arg.slice(name.length + 1);
if (!value) throw new Error(name + ' requires a value');
return value;
}
}
return undefined;
}

function parseSetupHooksCliOptions(argv, env = process.env) {
const platform = optionValue(argv, '--platform');
const runtime = optionValue(argv, '--runtime');
if (platform && runtime && platform !== runtime) {
throw new Error(
`conflicting --platform=${platform} and --runtime=${runtime}; pass one runtime identity`
);
}
const rawRoot = optionValue(argv, '--root');
const root = rawRoot === undefined ? undefined : rawRoot.trim();
if (rawRoot !== undefined && !root) {
throw new Error('--root requires a non-empty path');
}
return {
platform: platform || runtime,
root: root
? path.resolve(expandHomePath(root, env))
: undefined,
force: argv.includes('--force'),
uninstall: argv.includes('--uninstall'),
verify: argv.includes('--verify'),
json: argv.includes('--json'),
};
}

function expandHomePath(value, env = process.env) {
if (value === '~') return env.HOME || require('os').homedir();
if (value.startsWith('~/') || value.startsWith('~\\')) {
Expand Down Expand Up @@ -68,6 +112,7 @@ function prepareProxyCliEnvironment(argv, env = process.env, dotenv = require('d
module.exports = {
applyProxyCliPathOptions,
expandHomePath,
parseSetupHooksCliOptions,
parseProxyCliPathOptions,
prepareProxyCliEnvironment,
};
75 changes: 61 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ if (process.argv[2] === 'proxy-token') {

const {
applyProxyCliPathOptions,
parseSetupHooksCliOptions,
prepareProxyCliEnvironment,
} = require('./cli-options');

Expand Down Expand Up @@ -3214,44 +3215,87 @@ async function main() {
const hookAdapter = require('./src/adapters/hookAdapter');
const { setupHooks, resolveConfigRoot, detectPlatform, loadAdapter } = hookAdapter;

const platformFlag = args.find(a => typeof a === 'string' && a.startsWith('--platform='));
const platform = platformFlag ? platformFlag.slice('--platform='.length) : undefined;
const force = args.includes('--force');
const uninstall = args.includes('--uninstall');
const verifyOnly = args.includes('--verify');
let setupOptions;
try {
setupOptions = parseSetupHooksCliOptions(args, process.env);
} catch (error) {
const message = error && error.message || String(error);
if (args.includes('--json')) {
process.stdout.write(JSON.stringify({
ok: false,
error: { code: 'invalid_arguments', message },
}) + '\n');
} else {
console.error('[setup-hooks] ' + message);
}
process.exit(2);
}
const {
platform,
root,
force,
uninstall,
verify: verifyOnly,
json: jsonOut,
} = setupOptions;
const failVerify = (code, message, exitCode) => {
if (jsonOut) {
process.stdout.write(JSON.stringify({
ok: false,
platform: platform || null,
config_root: root || null,
error: { code, message },
}) + '\n');
} else {
console.error('[setup-hooks] --verify: ' + message);
}
process.exit(exitCode);
};

if (verifyOnly) {
// Read-only verification: do not touch any files, just report whether
// the previously-installed hooks/plugin look healthy. Lets users answer
// "is the plugin actually loaded?" without grepping opencode logs.
try {
const platformId = platform || detectPlatform(process.cwd());
const platformId = platform || detectPlatform(root || process.cwd());
if (!platformId) {
console.error('[setup-hooks] --verify: could not detect platform. Pass --platform=opencode|cursor|claude-code|codex|kiro');
process.exit(2);
failVerify(
'platform_not_detected',
'could not detect platform. Pass --platform=opencode|cursor|claude-code|codex|kiro',
2
);
}
const adapter = loadAdapter(platformId);
if (!adapter || typeof adapter.verify !== 'function') {
console.error('[setup-hooks] --verify: platform ' + platformId + ' does not support verification yet.');
process.exit(2);
failVerify(
'verification_unsupported',
'platform ' + platformId + ' does not support verification yet.',
2
);
}
const configRoot = resolveConfigRoot(platformId, process.cwd());
const configRoot = root || resolveConfigRoot(platformId, process.cwd());
const report = adapter.verify({ configRoot });
if (typeof adapter.printVerifyReport === 'function') {
if (jsonOut) {
process.stdout.write(JSON.stringify(report) + '\n');
} else if (typeof adapter.printVerifyReport === 'function') {
adapter.printVerifyReport(report);
} else {
console.log(JSON.stringify(report, null, 2));
}
process.exit(report.ok ? 0 : 1);
} catch (verifyErr) {
console.error('[setup-hooks] --verify error:', verifyErr && verifyErr.message || verifyErr);
process.exit(1);
failVerify(
'verification_failed',
verifyErr && verifyErr.message || String(verifyErr),
1
);
}
}

try {
const result = await setupHooks({
platform,
configRoot: root,
cwd: process.cwd(),
force,
uninstall,
Expand Down Expand Up @@ -3683,9 +3727,12 @@ async function main() {
- --response-file=<path> (LLM response file for skill distillation)
- setup-hooks flags:
- --platform=cursor|claude-code|codex|kiro|opencode (auto-detect if omitted)
- --runtime=<platform> (deprecated alias for --platform)
- --root=<path> (explicit workspace/config root)
- --force (overwrite existing config)
- --uninstall (remove evolver hooks)
- --verify (read-only: print install health for the chosen platform)
- --json (with --verify: emit one machine-readable JSON object)
- asset-log flags:
- --run=<run_id> (filter by run ID)
- --action=<action> (filter: hub_search_hit, hub_search_miss, asset_reuse, asset_reference, asset_publish, asset_publish_skip)
Expand Down
102 changes: 100 additions & 2 deletions src/adapters/claudeCode.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const fs = require('fs');
const path = require('path');
const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir, isEvolverHookCommand, buildSafeNodeHookCommand } = require('./hookAdapter');
const { mergeJsonFile, copyHookScripts, verifyHookScriptCopies, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir, isEvolverHookCommand, buildSafeNodeHookCommand } = require('./hookAdapter');

const HOOK_SCRIPTS_DIR_NAME = 'hooks';
const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
Expand Down Expand Up @@ -118,6 +118,104 @@ function install({ configRoot, evolverRoot, force }) {
};
}

function verify({ configRoot }) {
const claudeDir = path.join(configRoot, '.claude');
const settingsPath = path.join(claudeDir, 'settings.json');
const hooksDir = path.join(claudeDir, HOOK_SCRIPTS_DIR_NAME);
const claudeMdPath = path.join(configRoot, 'CLAUDE.md');
const checks = [];
let settings = null;
let settingsError = null;
try {
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
} catch (error) {
settingsError = error && error.message || String(error);
}
checks.push({
id: 'settings_json_readable',
ok: settings !== null,
detail: settings ? settingsPath : `unreadable: ${settingsError}`,
});
checks.push({
id: 'managed_marker',
ok: settings?._evolver_managed === true,
detail: settings?._evolver_managed === true
? '_evolver_managed is true'
: 'settings.json is not marked as evolver-managed',
});
checks.push({
id: 'hooks_enabled',
ok: settings?.disableAllHooks !== true,
detail: settings?.disableAllHooks === true
? 'settings.json disables all hooks'
: 'hooks are not globally disabled',
});

const expectedHooks = buildClaudeHooks('', configRoot).hooks;
const missingCommands = [];
for (const [event, expectedMatchers] of Object.entries(expectedHooks)) {
const actualMatchers = Array.isArray(settings?.hooks?.[event])
? settings.hooks[event]
: [];
for (const expectedMatcher of expectedMatchers) {
const present = actualMatchers.some(actualMatcher => {
if ((actualMatcher?.matcher ?? null) !== (expectedMatcher.matcher ?? null)) {
return false;
}
if (!Array.isArray(actualMatcher?.hooks)) return false;
return expectedMatcher.hooks.every(expectedHook =>
actualMatcher.hooks.some(actualHook =>
actualHook?.type === expectedHook.type &&
actualHook?.command === expectedHook.command &&
actualHook?.timeout === expectedHook.timeout
)
);
});
if (!present) {
const command = expectedMatcher.hooks[0]?.command || event;
missingCommands.push(`${event}:${path.basename(command.split(' ').pop() || command)}`);
}
}
}
checks.push({
id: 'hooks_registered',
ok: missingCommands.length === 0,
detail: missingCommands.length === 0
? 'all Claude Code hooks are registered'
: 'missing commands: ' + missingCommands.join(', '),
});

checks.push(verifyHookScriptCopies(hooksDir));

let hasMemorySection = false;
try {
hasMemorySection = fs.readFileSync(claudeMdPath, 'utf8').includes(EVOLVER_MARKER);
} catch { /* reported below */ }
checks.push({
id: 'claude_md_section',
ok: hasMemorySection,
detail: hasMemorySection
? 'CLAUDE.md contains the managed evolution section'
: 'CLAUDE.md is missing the managed evolution section',
});

return {
ok: checks.every(check => check.ok),
platform: 'claude-code',
config_root: configRoot,
settings_path: settingsPath,
hooks_dir: hooksDir,
checks,
};
}

function printVerifyReport(report) {
console.log('[claude-code] Verify report');
for (const check of report.checks) {
console.log(`[claude-code] ${check.ok ? '[OK] ' : '[FAIL]'} ${check.id} -- ${check.detail}`);
}
}

function uninstall({ configRoot }) {
const claudeDir = path.join(configRoot, '.claude');
const settingsPath = path.join(claudeDir, 'settings.json');
Expand Down Expand Up @@ -191,4 +289,4 @@ function uninstall({ configRoot }) {
return { ok: true, removed: changed };
}

module.exports = { install, uninstall, buildClaudeHooks };
module.exports = { install, uninstall, verify, printVerifyReport, buildClaudeHooks };
Loading
Loading