diff --git a/.well-known/mcp/server-card.json b/.well-known/mcp/server-card.json index 49028fe..72acb4c 100644 --- a/.well-known/mcp/server-card.json +++ b/.well-known/mcp/server-card.json @@ -1,7 +1,7 @@ { "serverInfo": { "name": "pilotprotocol-mcp", - "version": "0.2.12" + "version": "0.2.13" }, "authentication": { "scheme": "local-daemon", diff --git a/CHANGELOG.md b/CHANGELOG.md index 285c307..e34f0f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to the `pilotprotocol-mcp` npm adapter are documented here. ## [Unreleased] +## [0.2.13] - 2026-08-07 + +### Fixed +- `attach --all` skips an unavailable optional OpenClaw host without abandoning every other harness. +- PicoClaw is only reported as attached when its host configuration exists; explicit PicoClaw attachment now fails precisely instead of silently doing nothing. + ## [0.2.12] - 2026-08-07 ### Fixed diff --git a/package-lock.json b/package-lock.json index eafe08a..13cc192 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pilotprotocol-mcp", - "version": "0.2.12", + "version": "0.2.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pilotprotocol-mcp", - "version": "0.2.12", + "version": "0.2.13", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", diff --git a/package.json b/package.json index 5b45f52..add2ff1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pilotprotocol-mcp", - "version": "0.2.12", + "version": "0.2.13", "mcpName": "io.github.pilot-protocol/pilot-mcp", "description": "Your agent's overlay network. MCP server exposing 436 Pilot specialist agents + P2P A2A messaging. One install configures every harness on your machine.", "type": "module", diff --git a/server.json b/server.json index e23223a..881600c 100644 --- a/server.json +++ b/server.json @@ -7,13 +7,13 @@ "url": "https://github.com/pilot-protocol/pilot-mcp", "source": "github" }, - "version": "0.2.12", + "version": "0.2.13", "websiteUrl": "https://pilotprotocol.network", "packages": [ { "registryType": "npm", "identifier": "pilotprotocol-mcp", - "version": "0.2.12", + "version": "0.2.13", "transport": { "type": "stdio" } } ] diff --git a/src/openclaw-plugin/evaluate.js b/src/openclaw-plugin/evaluate.js index bff11da..ef071fb 100644 --- a/src/openclaw-plugin/evaluate.js +++ b/src/openclaw-plugin/evaluate.js @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; -const PACKAGE_SPEC = 'pilotprotocol-mcp@0.2.12'; +const PACKAGE_SPEC = 'pilotprotocol-mcp@0.2.13'; const DEFAULT_TIMEOUT_MS = 20_000; const MAX_STDERR_BYTES = 1 << 20; diff --git a/src/openclaw-plugin/openclaw.plugin.json b/src/openclaw-plugin/openclaw.plugin.json index 0bdd163..a95d2b1 100644 --- a/src/openclaw-plugin/openclaw.plugin.json +++ b/src/openclaw-plugin/openclaw.plugin.json @@ -8,7 +8,7 @@ "pilot": { "transport": "stdio", "command": "npx", - "args": ["-y", "pilotprotocol-mcp@0.2.12"] + "args": ["-y", "pilotprotocol-mcp@0.2.13"] } } } diff --git a/src/setup/attach.js b/src/setup/attach.js index 20d34bc..bc21651 100644 --- a/src/setup/attach.js +++ b/src/setup/attach.js @@ -26,17 +26,32 @@ export async function runAttach(flags, options = {}) { const selected = selectedHarnesses(flags); const writers = options.harnesses ?? harnesses; const configured = []; + const skipped = []; for (const id of selected) { const writer = writers[id]; if (!writer?.configure) throw new Error(`unsupported harness ${id}`); - await writer.configure({ id, name: id, transport: 'managed', enterpriseControl: controlPath }); + const result = await writer.configure({ + id, + name: id, + transport: 'managed', + enterpriseControl: controlPath, + home, + allowMissingHost: flags.all === true, + }); + if (result?.skipped === true) { + skipped.push({ id, reason: result.reason || 'host is unavailable' }); + continue; + } configured.push(id); } const write = options.write ?? ((message) => process.stdout.write(`${message}\n`)); write(`Attached ${configured.join(', ')} to the existing core Pilot node.`); + if (skipped.length > 0) { + write(`Skipped ${skipped.map(({ id, reason }) => `${id} (${reason})`).join(', ')}.`); + } write('Core runtime and node identity were not changed.'); - return { controlPath, configured }; + return { controlPath, configured, skipped }; } export function selectedHarnesses(flags) { diff --git a/src/setup/harnesses/openclaw.js b/src/setup/harnesses/openclaw.js index 79f5e76..0dcdce1 100644 --- a/src/setup/harnesses/openclaw.js +++ b/src/setup/harnesses/openclaw.js @@ -9,34 +9,43 @@ import { homedir } from 'node:os'; import { fileURLToPath, URL } from 'node:url'; import { promisify } from 'node:util'; -const HOME = homedir(); -const CONFIG = join(HOME, '.openclaw', 'openclaw.json'); const SOURCE_PLUGIN = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..', 'openclaw-plugin'); -const INSTALLED_PLUGIN = join(HOME, '.pilot', 'integrations', 'openclaw-policy'); const execFileAsync = promisify(execFile); -export async function configure() { - removeObsoleteMcpEntry(); - mkdirSync(join(HOME, '.pilot', 'integrations'), { recursive: true }); - cpSync(SOURCE_PLUGIN, INSTALLED_PLUGIN, { recursive: true, force: true }); - await execFileAsync('openclaw', ['plugins', 'install', '--link', '--force', INSTALLED_PLUGIN], { - env: process.env, timeout: 60000, maxBuffer: 1 << 20, - }); - await execFileAsync('openclaw', ['plugins', 'enable', 'pilot-policy'], { - env: process.env, timeout: 60000, maxBuffer: 1 << 20, - }); - await execFileAsync('openclaw', ['plugins', 'inspect', 'pilot-policy', '--json'], { - env: process.env, timeout: 60000, maxBuffer: 1 << 20, - }); +export async function configure(options = {}) { + const home = options.home ?? homedir(); + const config = join(home, '.openclaw', 'openclaw.json'); + const installedPlugin = join(home, '.pilot', 'integrations', 'openclaw-policy'); + const execute = options.execFileAsync ?? execFileAsync; + removeObsoleteMcpEntry(config); + mkdirSync(join(home, '.pilot', 'integrations'), { recursive: true }); + cpSync(SOURCE_PLUGIN, installedPlugin, { recursive: true, force: true }); + try { + await execute('openclaw', ['plugins', 'install', '--link', '--force', installedPlugin], { + env: process.env, timeout: 60000, maxBuffer: 1 << 20, + }); + await execute('openclaw', ['plugins', 'enable', 'pilot-policy'], { + env: process.env, timeout: 60000, maxBuffer: 1 << 20, + }); + await execute('openclaw', ['plugins', 'inspect', 'pilot-policy', '--json'], { + env: process.env, timeout: 60000, maxBuffer: 1 << 20, + }); + } catch (error) { + if (options.allowMissingHost === true && error?.code === 'ENOENT') { + return { skipped: true, reason: 'OpenClaw CLI is not installed' }; + } + throw error; + } + return { skipped: false }; } -function removeObsoleteMcpEntry() { - if (!existsSync(CONFIG)) return; - const current = JSON.parse(readFileSync(CONFIG, 'utf8')); +function removeObsoleteMcpEntry(config) { + if (!existsSync(config)) return; + const current = JSON.parse(readFileSync(config, 'utf8')); if (isPilotMcp(current.mcpServers?.pilot)) { delete current.mcpServers.pilot; if (Object.keys(current.mcpServers).length === 0) delete current.mcpServers; - writeFileSync(CONFIG, JSON.stringify(current, null, 2)); + writeFileSync(config, JSON.stringify(current, null, 2)); } } diff --git a/src/setup/harnesses/picoclaw.js b/src/setup/harnesses/picoclaw.js index e3be917..8d8cfc0 100644 --- a/src/setup/harnesses/picoclaw.js +++ b/src/setup/harnesses/picoclaw.js @@ -8,12 +8,15 @@ import { join } from 'node:path'; import { homedir } from 'node:os'; import { PILOT_PACKAGE_SPEC, pilotMcpServer } from './runtime.js'; -const HOME = homedir(); -const CONFIG = join(HOME, '.picoclaw', 'config.json'); - -export async function configure() { - if (!existsSync(CONFIG)) return; - const current = JSON.parse(readFileSync(CONFIG, 'utf8')); +export async function configure(options = {}) { + const config = join(options.home ?? homedir(), '.picoclaw', 'config.json'); + if (!existsSync(config)) { + if (options.allowMissingHost === true) { + return { skipped: true, reason: 'PicoClaw configuration was not found' }; + } + throw new Error(`PicoClaw configuration was not found at ${config}`); + } + const current = JSON.parse(readFileSync(config, 'utf8')); current.tools = current.tools ?? {}; current.tools.mcp = current.tools.mcp ?? {}; current.tools.mcp.enabled = true; @@ -31,5 +34,6 @@ export async function configure() { command: ['npx', '-y', PILOT_PACKAGE_SPEC, 'picoclaw-hook'], intercept: ['before_tool', 'after_tool'], }; - writeFileSync(CONFIG, JSON.stringify(current, null, 2)); + writeFileSync(config, JSON.stringify(current, null, 2)); + return { skipped: false }; } diff --git a/src/version.js b/src/version.js index 3bb9c7f..306c5be 100644 --- a/src/version.js +++ b/src/version.js @@ -1,4 +1,4 @@ // One source of truth for runtime and generated-configuration versioning. // Release-contract tests keep this synchronized with package/registry metadata. -export const VERSION = '0.2.12'; +export const VERSION = '0.2.13'; export const PACKAGE_SPEC = `pilotprotocol-mcp@${VERSION}`; diff --git a/test/attach.test.js b/test/attach.test.js index 481f7e5..e8e7b6b 100644 --- a/test/attach.test.js +++ b/test/attach.test.js @@ -4,7 +4,7 @@ import { chmodSync, lstatSync, mkdirSync, mkdtempSync, writeFileSync } from 'nod import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { runAttach, selectedHarnesses } from '../src/setup/attach.js'; +import { ATTACHABLE_HARNESSES, runAttach, selectedHarnesses } from '../src/setup/attach.js'; test('adapter-only attach configures a harness without touching the core runtime', async () => { const home = mkdtempSync(join(tmpdir(), 'pilot-attach-')); @@ -24,11 +24,45 @@ test('adapter-only attach configures a harness without touching the core runtime }); assert.deepEqual(result.configured, ['gemini']); + assert.deepEqual(result.skipped, []); assert.equal(result.controlPath, control); - assert.deepEqual(calls, [{ id: 'gemini', name: 'gemini', transport: 'managed', enterpriseControl: control }]); + assert.deepEqual(calls, [{ + id: 'gemini', name: 'gemini', transport: 'managed', enterpriseControl: control, + home, allowMissingHost: false, + }]); assert.match(output.join('\n'), /Core runtime and node identity were not changed/); }); +test('attach --all skips a missing optional host without abandoning other harnesses', async () => { + const home = mkdtempSync(join(tmpdir(), 'pilot-attach-all-')); + const managed = join(home, '.pilot', 'managed'); + mkdirSync(managed, { recursive: true, mode: 0o700 }); + const control = join(managed, 'enterprise-control.json'); + writeFileSync(control, '{"mode":"managed"}\n', { mode: 0o600 }); + chmodSync(control, 0o600); + const calls = []; + const output = []; + const harnesses = Object.fromEntries(ATTACHABLE_HARNESSES.map((id) => [id, { + configure: async (options) => { + calls.push(options); + return id === 'openclaw' ? { skipped: true, reason: 'OpenClaw CLI is not installed' } : undefined; + }, + }])); + + const result = await runAttach({ all: true }, { + home, + lstat: lstatSync, + harnesses, + write: (line) => output.push(line), + }); + + assert.equal(calls.length, ATTACHABLE_HARNESSES.length); + assert.equal(calls.every((call) => call.allowMissingHost === true && call.home === home), true); + assert.deepEqual(result.configured, ATTACHABLE_HARNESSES.filter((id) => id !== 'openclaw')); + assert.deepEqual(result.skipped, [{ id: 'openclaw', reason: 'OpenClaw CLI is not installed' }]); + assert.match(output.join('\n'), /Skipped openclaw \(OpenClaw CLI is not installed\)/); +}); + test('adapter-only attach requires an explicit harness and owner-only regular control', async () => { assert.throws(() => selectedHarnesses({}), /choose at least one harness/); await assert.rejects( diff --git a/test/harness-config-contracts.test.js b/test/harness-config-contracts.test.js index 40021b8..3cf6a1c 100644 --- a/test/harness-config-contracts.test.js +++ b/test/harness-config-contracts.test.js @@ -38,7 +38,7 @@ test('Claude separates user MCP registration from hook settings and migrates sta configureInHome('claude', home); const mcp = JSON.parse(readFileSync(join(home, '.claude.json'), 'utf8')); - assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']); const settings = JSON.parse(readFileSync(settingsPath, 'utf8')); assert.equal(settings.theme, 'dark'); assert.deepEqual(settings.mcpServers, { customer: { command: 'customer-mcp' } }); @@ -54,7 +54,7 @@ test('Gemini uses current MCP and BeforeTool/AfterTool user settings idempotentl writeJSON(settingsPath, { mcpServers: { customer: { command: 'customer-mcp' } }, hooks: {} }); configureInHome('gemini', home); const settings = JSON.parse(readFileSync(settingsPath, 'utf8')); - assert.deepEqual(settings.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(settings.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']); assert.equal(settings.mcpServers.customer.command, 'customer-mcp'); assert.equal(settings.hooksConfig.enabled, true); assert.equal(settings.hooks.BeforeTool.length, 1); @@ -75,7 +75,7 @@ test('Continue merges Pilot into config.yaml and removes only its obsolete dupli const config = parse(source); assert.match(source, /# customer config/); assert.equal(config.mcpServers.filter((entry) => entry.name === 'Pilot').length, 1); - assert.deepEqual(config.mcpServers.find((entry) => entry.name === 'Pilot').args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(config.mcpServers.find((entry) => entry.name === 'Pilot').args, ['-y', 'pilotprotocol-mcp@0.2.13']); assert.equal(config.mcpServers.find((entry) => entry.name === 'Customer').command, 'customer-mcp'); assert.equal(existsSync(legacyPath), false); }); @@ -91,7 +91,7 @@ test('OpenHands migrates pre-1.0 TOML MCP config and installs project hooks', () configureInHome('openhands', home, { cwd: workspace }); const mcp = JSON.parse(readFileSync(join(home, '.openhands', 'mcp.json'), 'utf8')); - assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']); assert.equal(mcp.mcpServers.customer.command, 'customer-mcp'); const legacy = readFileSync(legacyPath, 'utf8'); assert.doesNotMatch(legacy, /mcp\.stdio_servers\.pilot/); @@ -110,7 +110,7 @@ test('Codex upgrades its owned TOML table without duplicating user configuration configureInHome('codex', home); const config = readFileSync(configPath, 'utf8'); assert.equal(config.match(/\[mcp_servers\.pilot\]/g)?.length, 1); - assert.match(config, /pilotprotocol-mcp@0\.2\.12/); + assert.match(config, /pilotprotocol-mcp@0\.2\.13/); assert.match(config, /\[mcp_servers\.customer\]/); assert.match(config, /model = "customer"/); const hooks = JSON.parse(readFileSync(join(home, '.codex', 'hooks.json'), 'utf8')); @@ -122,6 +122,6 @@ test('Junie writes the shared CLI and IDE user MCP location', () => { const home = mkdtempSync(join(tmpdir(), 'pilot-junie-contract-')); configureInHome('junie', home); const config = JSON.parse(readFileSync(join(home, '.junie', 'mcp', 'mcp.json'), 'utf8')); - assert.deepEqual(config.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(config.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']); assert.equal(existsSync(join(home, '.junie', 'config.json')), false); }); diff --git a/test/hermes-setup.test.js b/test/hermes-setup.test.js index 25aae3a..e3b31c8 100644 --- a/test/hermes-setup.test.js +++ b/test/hermes-setup.test.js @@ -23,12 +23,12 @@ test('Hermes setup merges native pre/post hooks without replacing existing YAML' assert.equal(result.model, 'gemini/example'); assert.equal(result.hooks.on_session_start[0].command, 'existing-hook'); assert.equal(result.hooks.pre_tool_call.length, 1); - assert.equal(result.hooks.pre_tool_call[0].command, 'npx -y pilotprotocol-mcp@0.2.12 hook --harness hermes --phase pre'); + assert.equal(result.hooks.pre_tool_call[0].command, 'npx -y pilotprotocol-mcp@0.2.13 hook --harness hermes --phase pre'); assert.equal(result.hooks.post_tool_call.length, 1); - assert.deepEqual(result.mcp_servers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(result.mcp_servers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']); const allowlist = JSON.parse(readFileSync(join(home, '.hermes', 'shell-hooks-allowlist.json'), 'utf8')); assert.deepEqual(allowlist.approvals, [ - { event: 'pre_tool_call', command: 'npx -y pilotprotocol-mcp@0.2.12 hook --harness hermes --phase pre' }, - { event: 'post_tool_call', command: 'npx -y pilotprotocol-mcp@0.2.12 hook --harness hermes --phase post' }, + { event: 'pre_tool_call', command: 'npx -y pilotprotocol-mcp@0.2.13 hook --harness hermes --phase pre' }, + { event: 'post_tool_call', command: 'npx -y pilotprotocol-mcp@0.2.13 hook --harness hermes --phase post' }, ]); }); diff --git a/test/native-harness-setup.test.js b/test/native-harness-setup.test.js index 6c43c37..4b8ecd9 100644 --- a/test/native-harness-setup.test.js +++ b/test/native-harness-setup.test.js @@ -20,7 +20,7 @@ test('Cursor setup installs an idempotent fail-closed native tool boundary', () const hooks = JSON.parse(readFileSync(join(home, '.cursor', 'hooks.json'), 'utf8')); assert.equal(hooks.hooks.preToolUse.length, 1); assert.equal(hooks.hooks.preToolUse[0].failClosed, true); - assert.match(hooks.hooks.preToolUse[0].command, /^npx -y pilotprotocol-mcp@0\.2\.12 /); + assert.match(hooks.hooks.preToolUse[0].command, /^npx -y pilotprotocol-mcp@0\.2\.13 /); assert.match(hooks.hooks.preToolUse[0].command, /--harness cursor --phase pre/); }); @@ -29,11 +29,11 @@ test('Cline setup installs current global pre/post hook shims without replacing configureInHome('cline', home); const pre = readFileSync(join(home, '.cline', 'hooks', 'PreToolUse'), 'utf8'); const post = readFileSync(join(home, '.cline', 'hooks', 'PostToolUse'), 'utf8'); - assert.match(pre, /exec npx -y pilotprotocol-mcp@0\.2\.12 hook/); + assert.match(pre, /exec npx -y pilotprotocol-mcp@0\.2\.13 hook/); assert.match(pre, /--harness cline --phase pre/); assert.match(post, /--harness cline --phase post/); const mcp = JSON.parse(readFileSync(join(home, '.cline', 'data', 'settings', 'cline_mcp_settings.json'), 'utf8')); - assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']); const conflictHome = mkdtempSync(join(tmpdir(), 'pilot-cline-conflict-')); const target = join(conflictHome, '.cline', 'hooks', 'PreToolUse'); @@ -53,7 +53,7 @@ test('Cline setup emits the only Windows hook filename and PowerShell contract C cwd: process.cwd(), env: { ...process.env, HOME: home }, stdio: 'pipe', }); const source = readFileSync(join(home, '.cline', 'hooks', 'PreToolUse.ps1'), 'utf8'); - assert.match(source, /^& npx -y pilotprotocol-mcp@0\.2\.12 hook --harness cline --phase pre/m); + assert.match(source, /^& npx -y pilotprotocol-mcp@0\.2\.13 hook --harness cline --phase pre/m); assert.match(source, /LASTEXITCODE/); }); @@ -64,10 +64,10 @@ test('Copilot setup writes the documented cross-platform command-hook fields', ( const pre = hooks.hooks.preToolUse[0]; assert.equal(pre.type, 'command'); assert.equal(pre.bash, pre.powershell); - assert.match(pre.bash, /^npx -y pilotprotocol-mcp@0\.2\.12 /); + assert.match(pre.bash, /^npx -y pilotprotocol-mcp@0\.2\.13 /); assert.equal(pre.command, undefined); const mcp = JSON.parse(readFileSync(join(home, '.copilot', 'mcp-config.json'), 'utf8')); - assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(mcp.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']); }); test('PicoClaw setup attaches the native process hook as a fixed argv array', () => { @@ -80,10 +80,20 @@ test('PicoClaw setup attaches the native process hook as a fixed argv array', () const config = JSON.parse(readFileSync(join(home, '.picoclaw', 'config.json'), 'utf8')); assert.equal(config.tools.mcp.enabled, true); assert.equal(config.tools.mcp.servers.pilot.enabled, true); - assert.deepEqual(config.hooks.processes.pilot.command, ['npx', '-y', 'pilotprotocol-mcp@0.2.12', 'picoclaw-hook']); + assert.deepEqual(config.hooks.processes.pilot.command, ['npx', '-y', 'pilotprotocol-mcp@0.2.13', 'picoclaw-hook']); assert.deepEqual(config.hooks.processes.pilot.intercept, ['before_tool', 'after_tool']); }); +test('PicoClaw setup never reports a nonexistent installation as configured', async () => { + const home = mkdtempSync(join(tmpdir(), 'pilot-pico-missing-')); + const { configure } = await import('../src/setup/harnesses/picoclaw.js'); + assert.deepEqual( + await configure({ home, allowMissingHost: true }), + { skipped: true, reason: 'PicoClaw configuration was not found' }, + ); + await assert.rejects(configure({ home }), /PicoClaw configuration was not found/); +}); + test('OpenClaw setup installs the bundled native policy plugin in one pass', () => { const home = mkdtempSync(join(tmpdir(), 'pilot-openclaw-home-')); const bin = join(home, 'bin'); @@ -104,5 +114,27 @@ test('OpenClaw setup installs the bundled native policy plugin in one pass', () assert.match(calls, /plugins\nenable\npilot-policy/); assert.match(calls, /plugins\ninspect\npilot-policy\n--json/); const manifest = JSON.parse(readFileSync(join(installed, 'openclaw.plugin.json'), 'utf8')); - assert.deepEqual(manifest.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.12']); + assert.deepEqual(manifest.mcpServers.pilot.args, ['-y', 'pilotprotocol-mcp@0.2.13']); +}); + +test('OpenClaw setup reports a missing optional host during attach --all', async () => { + const home = mkdtempSync(join(tmpdir(), 'pilot-openclaw-missing-')); + const { configure } = await import('../src/setup/harnesses/openclaw.js'); + const missingHost = Object.assign(new Error('spawn openclaw ENOENT'), { code: 'ENOENT' }); + const result = await configure({ + home, + allowMissingHost: true, + execFileAsync: async () => { throw missingHost; }, + }); + + assert.deepEqual(result, { skipped: true, reason: 'OpenClaw CLI is not installed' }); + assert.equal(existsSync(join(home, '.pilot', 'integrations', 'openclaw-policy', 'openclaw.plugin.json')), true); + + await assert.rejects( + configure({ + home: mkdtempSync(join(tmpdir(), 'pilot-openclaw-required-')), + execFileAsync: async () => { throw missingHost; }, + }), + /spawn openclaw ENOENT/, + ); }); diff --git a/test/openclaw-plugin.test.js b/test/openclaw-plugin.test.js index cc44bfa..05b4c7d 100644 --- a/test/openclaw-plugin.test.js +++ b/test/openclaw-plugin.test.js @@ -38,7 +38,7 @@ test('OpenClaw plugin invokes the pinned adapter and preserves the entire event' assert.deepEqual(result, { blocked: false }); assert.equal(observation.command, 'npx'); assert.deepEqual(observation.args, [ - '-y', 'pilotprotocol-mcp@0.2.12', 'hook', '--harness', 'openclaw', '--phase', 'pre', + '-y', 'pilotprotocol-mcp@0.2.13', 'hook', '--harness', 'openclaw', '--phase', 'pre', ]); assert.deepEqual(JSON.parse(observation.input), event); });