mcpill 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli.js +562 -24
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.0
4
+
5
+ - `trigger: UserPromptSubmit` is a valid hook trigger in `PILL.md` `## Hook:` sections and `.mcpill/server/hooks/*.md` files
6
+ - `mcpill compile` writes `UserPromptSubmit` hooks to `.claude/settings.json` without a `matcher` field (not supported by this event type)
7
+ - `matcher` or `blocking: true` on a `UserPromptSubmit` hook emits a compile-time warning and the field is dropped
8
+
9
+ ## 1.10.0
10
+
11
+ - `mcpill compile` emits `.mcpill/dist/<pill>/` — a standalone npm package containing `install.js`, `index.js`, `tools.js`, `package.json`, and (for blocking pills) `hooks/pre-tool-gate.js` + `hooks/post-analysis-flag.js`
12
+ - `<pill> install --agent <target>` wires the MCP server and blocking hooks into the target agent's config; supports `claude-code`, `cursor`, `windsurf`, `github-copilot`, `codex`; idempotent
13
+ - `<pill> uninstall --agent <target>` reverses the install cleanly
14
+ - `hooks/pre-tool-gate.js` normalises stdin from all four agent input shapes (Claude Code, Cursor, Windsurf, GitHub Copilot) and branches exit code / output per agent (exit 1, exit 2, or JSON deny)
15
+ - `index.js` in the standalone package wraps `required_tool` handlers to write the flag file, enabling enforcement on agents with no `PostToolUse` hook (Windsurf)
16
+ - Opt-in `postinstall: true` and `version` fields in `PILL.md ## Server` block
17
+ - `<pill> install` writes `~/.{pillName}/state.json { enabled: true }` so gate enforcement is active immediately after install; `uninstall` removes it
18
+ - `mcpill init` scaffolds `README.md` with a quickstart guide covering project layout and tool-editing workflow
19
+
3
20
  ## 1.9.0
4
21
 
5
22
  - `blocking: true` field on `PILL.md` `## Hook:` blocks; `mcpill compile` generates a `.mjs` `PreToolUse` gate script (exits 1 until a required tool has run) and a `PostToolUse` flag script
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
  import { fileURLToPath } from "url";
6
- import { dirname, join as join6 } from "path";
6
+ import { dirname, join as join7 } from "path";
7
7
  import { readFileSync as readFileSync5 } from "fs";
8
8
 
9
9
  // src/commands/init.ts
@@ -1076,8 +1076,13 @@ function mergeUserHooksIntoSettings(baseDir, hookBlocks) {
1076
1076
  ...block.matcher !== void 0 ? { matcher: block.matcher } : {},
1077
1077
  hooks: [{ type: "command", command: block.command }]
1078
1078
  };
1079
- const matcherKey = block.matcher ?? "";
1080
- const idx = existing.findIndex((e) => (e.matcher ?? "") === matcherKey);
1079
+ let idx;
1080
+ if (block.event === "UserPromptSubmit") {
1081
+ idx = existing.findIndex((e) => e.hooks[0]?.command === block.command);
1082
+ } else {
1083
+ const matcherKey = block.matcher ?? "";
1084
+ idx = existing.findIndex((e) => (e.matcher ?? "") === matcherKey);
1085
+ }
1081
1086
  if (idx !== -1) {
1082
1087
  existing[idx] = newEntry;
1083
1088
  } else {
@@ -1375,8 +1380,8 @@ async function runServer(opts) {
1375
1380
  }
1376
1381
 
1377
1382
  // src/commands/compile.ts
1378
- import { resolve, join as join5 } from "path";
1379
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
1383
+ import { resolve, join as join6 } from "path";
1384
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
1380
1385
  import { pathToFileURL as pathToFileURL2 } from "url";
1381
1386
 
1382
1387
  // src/compiler/serialize.ts
@@ -1548,6 +1553,514 @@ function mergeHandlers(doc, existing) {
1548
1553
  return { doc: { ...doc, tools }, stubbed };
1549
1554
  }
1550
1555
 
1556
+ // src/compiler/standalone.ts
1557
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1558
+ import { join as join5 } from "path";
1559
+ function generateStandaloneIndexJs(pillName, version, prompts, resources, blockingHookTools) {
1560
+ const promptsJson = JSON.stringify(prompts, null, 2);
1561
+ const resourcesJson = JSON.stringify(resources, null, 2);
1562
+ const lines = [
1563
+ `#!/usr/bin/env node`,
1564
+ `import { createServer } from 'mcpster';`,
1565
+ `import { z } from 'mcpill-runtime';`,
1566
+ `import { writeFileSync, mkdirSync } from 'fs';`,
1567
+ `import { homedir } from 'os';`,
1568
+ `import { join } from 'path';`,
1569
+ `import tools from './tools.js';`,
1570
+ ``,
1571
+ `const PILL_NAME = ${JSON.stringify(pillName)};`,
1572
+ `const PILL_VERSION = ${JSON.stringify(version)};`,
1573
+ `const REQUIRED_TOOLS = ${JSON.stringify(blockingHookTools)};`,
1574
+ ``,
1575
+ `const prompts = ${promptsJson};`,
1576
+ `const resources = ${resourcesJson};`
1577
+ ];
1578
+ if (blockingHookTools.length > 0) {
1579
+ lines.push(
1580
+ ``,
1581
+ `const _writeFlagFile = (pillName, toolName) => {`,
1582
+ ` const dir = join(homedir(), '.' + pillName);`,
1583
+ ` try {`,
1584
+ ` mkdirSync(dir, { recursive: true });`,
1585
+ ` writeFileSync(join(dir, 'last_' + toolName + '_run.json'), JSON.stringify({ timestamp: new Date().toISOString() }));`,
1586
+ ` } catch {}`,
1587
+ `};`
1588
+ );
1589
+ }
1590
+ lines.push(
1591
+ ``,
1592
+ `const server = createServer({ name: PILL_NAME, version: PILL_VERSION, transport: 'stdio' });`,
1593
+ ``,
1594
+ `for (const { name, description, schema, handler } of tools) {`,
1595
+ ` server.defineTool({`,
1596
+ ` name,`,
1597
+ ` description,`,
1598
+ ` schema: z.object(schema),`
1599
+ );
1600
+ if (blockingHookTools.length > 0) {
1601
+ lines.push(
1602
+ ` handler: REQUIRED_TOOLS.includes(name)`,
1603
+ ` ? async (args) => { const r = await handler(args); _writeFlagFile(PILL_NAME, name); return r; }`,
1604
+ ` : handler,`
1605
+ );
1606
+ } else {
1607
+ lines.push(` handler,`);
1608
+ }
1609
+ lines.push(
1610
+ ` });`,
1611
+ `}`,
1612
+ ``,
1613
+ `for (const { name: promptName, description, messages } of prompts) {`,
1614
+ ` server.definePrompt({`,
1615
+ ` name: promptName,`,
1616
+ ` description,`,
1617
+ ` handler: async (args) =>`,
1618
+ ` messages.map(m => m.content.replace(/{{(\\w+)}}/g, (_, k) => String(args[k] ?? ''))).join('\\n'),`,
1619
+ ` });`,
1620
+ `}`,
1621
+ ``,
1622
+ `for (const { uri, name: resourceName, content } of resources) {`,
1623
+ ` server.defineResource({`,
1624
+ ` uri,`,
1625
+ ` description: resourceName ?? uri,`,
1626
+ ` resolver: async () => content,`,
1627
+ ` });`,
1628
+ `}`,
1629
+ ``,
1630
+ `await server.start();`,
1631
+ ``
1632
+ );
1633
+ return lines.join("\n");
1634
+ }
1635
+ function generateStandaloneGateScript(pillName, blockingHooks) {
1636
+ const hooksData = blockingHooks.map((h) => ({
1637
+ required_tool: h.required_tool,
1638
+ gate_window_seconds: h.gate_window_seconds ?? 120,
1639
+ instructions: h.instructions ?? `[mcpill:${pillName}] Run mcp__${pillName}__${h.required_tool} before calling other tools.`
1640
+ }));
1641
+ return [
1642
+ `#!/usr/bin/env node`,
1643
+ `import { readFileSync, existsSync } from 'fs';`,
1644
+ `import { homedir } from 'os';`,
1645
+ `import { join } from 'path';`,
1646
+ ``,
1647
+ `const PILL_NAME = ${JSON.stringify(pillName)};`,
1648
+ `const HOOKS = ${JSON.stringify(hooksData, null, 2)};`,
1649
+ ``,
1650
+ `const raw = readFileSync(0, 'utf-8');`,
1651
+ `const input = JSON.parse(raw);`,
1652
+ ``,
1653
+ `// Agent detection + tool name normalization`,
1654
+ `let toolName;`,
1655
+ `if (typeof input.tool_info === 'object' && input.tool_info !== null && typeof input.tool_info.mcp_tool_name === 'string') {`,
1656
+ ` toolName = input.tool_info.mcp_tool_name; // Windsurf`,
1657
+ `} else if (typeof input.toolName === 'string') {`,
1658
+ ` toolName = input.toolName; // GitHub Copilot`,
1659
+ `} else {`,
1660
+ ` toolName = input.tool_name ?? ''; // Claude Code, Cursor, Codex`,
1661
+ `}`,
1662
+ ``,
1663
+ `if (toolName.startsWith('mcp__' + PILL_NAME + '__')) process.exit(0);`,
1664
+ ``,
1665
+ `const unsatisfied = [];`,
1666
+ `for (const hook of HOOKS) {`,
1667
+ ` const stateFile = join(homedir(), '.' + PILL_NAME, 'state.json');`,
1668
+ ` if (!existsSync(stateFile)) continue;`,
1669
+ ` const state = JSON.parse(readFileSync(stateFile, 'utf-8'));`,
1670
+ ` if (state.enabled === false) continue;`,
1671
+ ` const flagFile = join(homedir(), '.' + PILL_NAME, 'last_' + hook.required_tool + '_run.json');`,
1672
+ ` if (existsSync(flagFile)) {`,
1673
+ ` const flag = JSON.parse(readFileSync(flagFile, 'utf-8'));`,
1674
+ ` const elapsedMs = Date.now() - new Date(flag.timestamp).getTime();`,
1675
+ ` if (elapsedMs <= hook.gate_window_seconds * 1000) continue;`,
1676
+ ` }`,
1677
+ ` unsatisfied.push(hook.instructions);`,
1678
+ `}`,
1679
+ ``,
1680
+ `if (unsatisfied.length === 0) process.exit(0);`,
1681
+ ``,
1682
+ `const blockMsg = unsatisfied.join('\\n');`,
1683
+ ``,
1684
+ `// Per-agent output format`,
1685
+ `if (typeof input.tool_info === 'object' && input.tool_info !== null && typeof input.tool_info.mcp_tool_name === 'string') {`,
1686
+ ` // Windsurf: exit 2`,
1687
+ ` process.stderr.write(blockMsg + '\\n');`,
1688
+ ` process.exit(2);`,
1689
+ `} else if (typeof input.toolName === 'string') {`,
1690
+ ` // GitHub Copilot: JSON deny`,
1691
+ ` process.stdout.write(JSON.stringify({ permissionDecision: 'deny', permissionDecisionReason: blockMsg }));`,
1692
+ ` process.exit(0);`,
1693
+ `} else if (input.workspace_roots !== undefined || typeof input.conversation_id === 'string') {`,
1694
+ ` // Cursor: JSON deny`,
1695
+ ` process.stdout.write(JSON.stringify({ permission: 'deny', agentMessage: blockMsg }));`,
1696
+ ` process.exit(0);`,
1697
+ `} else {`,
1698
+ ` // Claude Code / Codex: exit 1`,
1699
+ ` process.stderr.write(blockMsg + '\\n');`,
1700
+ ` process.exit(1);`,
1701
+ `}`,
1702
+ ``
1703
+ ].join("\n");
1704
+ }
1705
+ function generateStandaloneFlagScript(pillName) {
1706
+ return [
1707
+ `#!/usr/bin/env node`,
1708
+ `import { writeFileSync, mkdirSync } from 'fs';`,
1709
+ `import { homedir } from 'os';`,
1710
+ `import { join } from 'path';`,
1711
+ ``,
1712
+ `const PILL_NAME = ${JSON.stringify(pillName)};`,
1713
+ ``,
1714
+ `const toolIdx = process.argv.indexOf('--tool');`,
1715
+ `const toolName = toolIdx !== -1 ? process.argv[toolIdx + 1] : null;`,
1716
+ ``,
1717
+ `if (!toolName) {`,
1718
+ ` process.stderr.write('[' + PILL_NAME + '] error: --tool <name> required\\n');`,
1719
+ ` process.exit(1);`,
1720
+ `}`,
1721
+ ``,
1722
+ `const dir = join(homedir(), '.' + PILL_NAME);`,
1723
+ `const flagFile = join(dir, 'last_' + toolName + '_run.json');`,
1724
+ ``,
1725
+ `try {`,
1726
+ ` mkdirSync(dir, { recursive: true });`,
1727
+ ` writeFileSync(flagFile, JSON.stringify({ timestamp: new Date().toISOString() }));`,
1728
+ `} catch (e) {`,
1729
+ ` process.stderr.write('[' + PILL_NAME + '] warning: could not write flag file: ' + e.message + '\\n');`,
1730
+ `}`,
1731
+ ``
1732
+ ].join("\n");
1733
+ }
1734
+ var INSTALL_JS_BODY = `
1735
+ function readJson(filePath) {
1736
+ if (!existsSync(filePath)) return {};
1737
+ try {
1738
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
1739
+ } catch {
1740
+ const bakPath = filePath + '.bak';
1741
+ try { writeFileSync(bakPath, readFileSync(filePath)); } catch {}
1742
+ process.stderr.write('[mcpill] warning: ' + filePath + ' was not valid JSON \u2014 backed up to ' + bakPath + ' and recreated\\n');
1743
+ return {};
1744
+ }
1745
+ }
1746
+
1747
+ function writeJson(filePath, obj) {
1748
+ const dir = dirname(filePath);
1749
+ mkdirSync(dir, { recursive: true });
1750
+ writeFileSync(filePath, JSON.stringify(obj, null, 2) + '\\n');
1751
+ }
1752
+
1753
+ function activatePill() {
1754
+ mkdirSync(dirname(STATE_FILE), { recursive: true });
1755
+ writeJson(STATE_FILE, { enabled: true });
1756
+ }
1757
+
1758
+ function deactivatePill() {
1759
+ try { rmSync(STATE_FILE, { force: true }); } catch {}
1760
+ }
1761
+
1762
+ function installClaudeCode() {
1763
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
1764
+ const wasInstalled = existsSync(settingsPath) && (readJson(settingsPath).mcpServers || {})[PILL_NAME] !== undefined;
1765
+ const s = readJson(settingsPath);
1766
+ if (!s.mcpServers) s.mcpServers = {};
1767
+ s.mcpServers[PILL_NAME] = MCP_ENTRY;
1768
+ if (HAS_BLOCKING_HOOKS) {
1769
+ if (!s.hooks) s.hooks = {};
1770
+ if (!s.hooks.PreToolUse) s.hooks.PreToolUse = [];
1771
+ if (!s.hooks.PostToolUse) s.hooks.PostToolUse = [];
1772
+ const gateMarker = '/' + PILL_NAME + '/hooks/';
1773
+ s.hooks.PreToolUse = s.hooks.PreToolUse.filter(e => !e.hooks?.[0]?.command?.includes(gateMarker));
1774
+ s.hooks.PostToolUse = s.hooks.PostToolUse.filter(e => !e.matcher?.startsWith('mcp__' + PILL_NAME + '__'));
1775
+ s.hooks.PreToolUse.push({ matcher: '.*', hooks: [{ type: 'command', command: 'node "' + GATE_PATH + '"' }] });
1776
+ for (const hook of HOOKS) {
1777
+ s.hooks.PostToolUse.push({
1778
+ matcher: 'mcp__' + PILL_NAME + '__' + hook.required_tool,
1779
+ hooks: [{ type: 'command', command: 'node "' + FLAG_PATH + '" --tool ' + hook.required_tool }],
1780
+ });
1781
+ }
1782
+ }
1783
+ writeJson(settingsPath, s);
1784
+ activatePill();
1785
+ if (!silent) process.stdout.write('[mcpill] info: ' + (wasInstalled ? 'reinstalled' : 'installed') + ' ' + PILL_NAME + (HAS_BLOCKING_HOOKS ? '' : ' (no blocking hooks \u2014 MCP server only)') + ' for claude-code\\n');
1786
+ }
1787
+
1788
+ function uninstallClaudeCode() {
1789
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
1790
+ if (!existsSync(settingsPath)) return;
1791
+ const s = readJson(settingsPath);
1792
+ if (s.mcpServers) delete s.mcpServers[PILL_NAME];
1793
+ if (s.hooks) {
1794
+ const gateMarker = '/' + PILL_NAME + '/hooks/';
1795
+ if (s.hooks.PreToolUse) s.hooks.PreToolUse = s.hooks.PreToolUse.filter(e => !e.hooks?.[0]?.command?.includes(gateMarker));
1796
+ if (s.hooks.PostToolUse) s.hooks.PostToolUse = s.hooks.PostToolUse.filter(e => !e.matcher?.startsWith('mcp__' + PILL_NAME + '__'));
1797
+ }
1798
+ writeJson(settingsPath, s);
1799
+ deactivatePill();
1800
+ if (!silent) process.stdout.write('[mcpill] info: uninstalled ' + PILL_NAME + ' from claude-code\\n');
1801
+ }
1802
+
1803
+ function installCursor() {
1804
+ const mcpPath = join(homedir(), '.cursor', 'mcp.json');
1805
+ const mcp = readJson(mcpPath);
1806
+ if (!mcp.mcpServers) mcp.mcpServers = {};
1807
+ mcp.mcpServers[PILL_NAME] = MCP_ENTRY;
1808
+ writeJson(mcpPath, mcp);
1809
+ if (HAS_BLOCKING_HOOKS) {
1810
+ const hooksPath = join(homedir(), '.cursor', 'hooks.json');
1811
+ const h = readJson(hooksPath);
1812
+ if (!h.hooks) h.hooks = {};
1813
+ if (!h.hooks.beforeMCPExecution) h.hooks.beforeMCPExecution = [];
1814
+ if (!h.hooks.afterMCPExecution) h.hooks.afterMCPExecution = [];
1815
+ const gateMarker = '/' + PILL_NAME + '/hooks/';
1816
+ h.hooks.beforeMCPExecution = h.hooks.beforeMCPExecution.filter(e => !e.command?.includes(gateMarker));
1817
+ h.hooks.afterMCPExecution = h.hooks.afterMCPExecution.filter(e => !e.command?.includes(gateMarker));
1818
+ h.hooks.beforeMCPExecution.push({ command: 'node "' + GATE_PATH + '"', type: 'command', matcher: '.*' });
1819
+ for (const hook of HOOKS) {
1820
+ h.hooks.afterMCPExecution.push({
1821
+ command: 'node "' + FLAG_PATH + '" --tool ' + hook.required_tool,
1822
+ type: 'command',
1823
+ matcher: 'mcp__' + PILL_NAME + '__' + hook.required_tool,
1824
+ });
1825
+ }
1826
+ writeJson(hooksPath, h);
1827
+ }
1828
+ activatePill();
1829
+ if (!silent) process.stdout.write('[mcpill] info: installed ' + PILL_NAME + ' for cursor\\n');
1830
+ }
1831
+
1832
+ function uninstallCursor() {
1833
+ const mcpPath = join(homedir(), '.cursor', 'mcp.json');
1834
+ if (existsSync(mcpPath)) {
1835
+ const mcp = readJson(mcpPath);
1836
+ if (mcp.mcpServers) delete mcp.mcpServers[PILL_NAME];
1837
+ writeJson(mcpPath, mcp);
1838
+ }
1839
+ const hooksPath = join(homedir(), '.cursor', 'hooks.json');
1840
+ if (existsSync(hooksPath)) {
1841
+ const h = readJson(hooksPath);
1842
+ const gateMarker = '/' + PILL_NAME + '/hooks/';
1843
+ if (h.hooks?.beforeMCPExecution) h.hooks.beforeMCPExecution = h.hooks.beforeMCPExecution.filter(e => !e.command?.includes(gateMarker));
1844
+ if (h.hooks?.afterMCPExecution) h.hooks.afterMCPExecution = h.hooks.afterMCPExecution.filter(e => !e.command?.includes(gateMarker));
1845
+ writeJson(hooksPath, h);
1846
+ }
1847
+ deactivatePill();
1848
+ if (!silent) process.stdout.write('[mcpill] info: uninstalled ' + PILL_NAME + ' from cursor\\n');
1849
+ }
1850
+
1851
+ function installWindsurf() {
1852
+ const mcpPath = join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
1853
+ const mcp = readJson(mcpPath);
1854
+ if (!mcp.mcpServers) mcp.mcpServers = {};
1855
+ mcp.mcpServers[PILL_NAME] = MCP_ENTRY;
1856
+ writeJson(mcpPath, mcp);
1857
+ if (HAS_BLOCKING_HOOKS) {
1858
+ const hooksPath = join(homedir(), '.codeium', 'windsurf', 'hooks.json');
1859
+ const h = readJson(hooksPath);
1860
+ if (!h.hooks) h.hooks = {};
1861
+ if (!h.hooks.pre_mcp_tool_use) h.hooks.pre_mcp_tool_use = [];
1862
+ const gateMarker = '/' + PILL_NAME + '/hooks/';
1863
+ h.hooks.pre_mcp_tool_use = h.hooks.pre_mcp_tool_use.filter(e => !e.command?.includes(gateMarker));
1864
+ h.hooks.pre_mcp_tool_use.push({ command: 'node "' + GATE_PATH + '"' });
1865
+ writeJson(hooksPath, h);
1866
+ }
1867
+ activatePill();
1868
+ if (!silent) process.stdout.write('[mcpill] info: installed ' + PILL_NAME + ' for windsurf\\n');
1869
+ }
1870
+
1871
+ function uninstallWindsurf() {
1872
+ const mcpPath = join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
1873
+ if (existsSync(mcpPath)) {
1874
+ const mcp = readJson(mcpPath);
1875
+ if (mcp.mcpServers) delete mcp.mcpServers[PILL_NAME];
1876
+ writeJson(mcpPath, mcp);
1877
+ }
1878
+ const hooksPath = join(homedir(), '.codeium', 'windsurf', 'hooks.json');
1879
+ if (existsSync(hooksPath)) {
1880
+ const h = readJson(hooksPath);
1881
+ const gateMarker = '/' + PILL_NAME + '/hooks/';
1882
+ if (h.hooks?.pre_mcp_tool_use) h.hooks.pre_mcp_tool_use = h.hooks.pre_mcp_tool_use.filter(e => !e.command?.includes(gateMarker));
1883
+ writeJson(hooksPath, h);
1884
+ }
1885
+ deactivatePill();
1886
+ if (!silent) process.stdout.write('[mcpill] info: uninstalled ' + PILL_NAME + ' from windsurf\\n');
1887
+ }
1888
+
1889
+ function installCopilot() {
1890
+ const mcpPath = join(homedir(), '.vscode', 'mcp.json');
1891
+ const mcp = readJson(mcpPath);
1892
+ if (!mcp.mcpServers) mcp.mcpServers = {};
1893
+ mcp.mcpServers[PILL_NAME] = MCP_ENTRY;
1894
+ writeJson(mcpPath, mcp);
1895
+ if (HAS_BLOCKING_HOOKS) {
1896
+ const hooksPath = join(homedir(), '.copilot', 'settings.json');
1897
+ const h = readJson(hooksPath);
1898
+ if (!h.hooks) h.hooks = {};
1899
+ if (!h.hooks.preToolUse) h.hooks.preToolUse = [];
1900
+ if (!h.hooks.postToolUse) h.hooks.postToolUse = [];
1901
+ const gateMarker = '/' + PILL_NAME + '/hooks/';
1902
+ h.hooks.preToolUse = h.hooks.preToolUse.filter(e => !e.bash?.includes(gateMarker));
1903
+ h.hooks.postToolUse = h.hooks.postToolUse.filter(e => !e.bash?.includes(gateMarker));
1904
+ h.hooks.preToolUse.push({ type: 'command', bash: 'node "' + GATE_PATH + '"', timeoutSec: 30 });
1905
+ for (const hook of HOOKS) {
1906
+ h.hooks.postToolUse.push({
1907
+ type: 'command',
1908
+ bash: 'node "' + FLAG_PATH + '" --tool ' + hook.required_tool,
1909
+ timeoutSec: 30,
1910
+ });
1911
+ }
1912
+ writeJson(hooksPath, h);
1913
+ }
1914
+ activatePill();
1915
+ if (!silent) process.stdout.write('[mcpill] info: installed ' + PILL_NAME + ' for github-copilot\\n');
1916
+ }
1917
+
1918
+ function uninstallCopilot() {
1919
+ const mcpPath = join(homedir(), '.vscode', 'mcp.json');
1920
+ if (existsSync(mcpPath)) {
1921
+ const mcp = readJson(mcpPath);
1922
+ if (mcp.mcpServers) delete mcp.mcpServers[PILL_NAME];
1923
+ writeJson(mcpPath, mcp);
1924
+ }
1925
+ const hooksPath = join(homedir(), '.copilot', 'settings.json');
1926
+ if (existsSync(hooksPath)) {
1927
+ const h = readJson(hooksPath);
1928
+ const gateMarker = '/' + PILL_NAME + '/hooks/';
1929
+ if (h.hooks?.preToolUse) h.hooks.preToolUse = h.hooks.preToolUse.filter(e => !e.bash?.includes(gateMarker));
1930
+ if (h.hooks?.postToolUse) h.hooks.postToolUse = h.hooks.postToolUse.filter(e => !e.bash?.includes(gateMarker));
1931
+ writeJson(hooksPath, h);
1932
+ }
1933
+ deactivatePill();
1934
+ if (!silent) process.stdout.write('[mcpill] info: uninstalled ' + PILL_NAME + ' from github-copilot\\n');
1935
+ }
1936
+
1937
+ function installCodex() {
1938
+ const configPath = join(homedir(), '.codex', 'config.toml');
1939
+ mkdirSync(join(homedir(), '.codex'), { recursive: true });
1940
+ let existing = '';
1941
+ if (existsSync(configPath)) {
1942
+ try { existing = readFileSync(configPath, 'utf-8'); } catch {}
1943
+ }
1944
+ // Remove existing entry for this pill
1945
+ const pat = new RegExp('\\\\[mcp_servers\\\\.' + PILL_NAME.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\][\\\\s\\\\S]*?(?=\\\\[|$)', 'g');
1946
+ existing = existing.replace(pat, '').trimEnd();
1947
+ if (!existing.includes('[mcp_servers]')) existing += '\\n[mcp_servers]';
1948
+ existing += '\\n\\n[mcp_servers.' + PILL_NAME + ']\\ncommand = "node"\\nargs = [' + JSON.stringify(INDEX_PATH) + ']\\n';
1949
+ writeFileSync(configPath, existing.trim() + '\\n');
1950
+ activatePill();
1951
+ if (!silent) process.stdout.write('[mcpill] info: installed ' + PILL_NAME + ' for codex (MCP server only \u2014 hooks not yet supported for codex)\\n');
1952
+ }
1953
+
1954
+ function uninstallCodex() {
1955
+ const configPath = join(homedir(), '.codex', 'config.toml');
1956
+ if (!existsSync(configPath)) return;
1957
+ let content = readFileSync(configPath, 'utf-8');
1958
+ const pat = new RegExp('\\\\[mcp_servers\\\\.' + PILL_NAME.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\][\\\\s\\\\S]*?(?=\\\\[|$)', 'g');
1959
+ content = content.replace(pat, '').trim() + '\\n';
1960
+ writeFileSync(configPath, content);
1961
+ deactivatePill();
1962
+ if (!silent) process.stdout.write('[mcpill] info: uninstalled ' + PILL_NAME + ' from codex\\n');
1963
+ }
1964
+
1965
+ if (command === 'install') {
1966
+ switch (agent) {
1967
+ case 'claude-code': installClaudeCode(); break;
1968
+ case 'cursor': installCursor(); break;
1969
+ case 'windsurf': installWindsurf(); break;
1970
+ case 'github-copilot': installCopilot(); break;
1971
+ case 'codex': installCodex(); break;
1972
+ }
1973
+ } else {
1974
+ switch (agent) {
1975
+ case 'claude-code': uninstallClaudeCode(); break;
1976
+ case 'cursor': uninstallCursor(); break;
1977
+ case 'windsurf': uninstallWindsurf(); break;
1978
+ case 'github-copilot': uninstallCopilot(); break;
1979
+ case 'codex': uninstallCodex(); break;
1980
+ }
1981
+ }
1982
+ `;
1983
+ function generateStandaloneInstallJs(pillName, version, blockingHooks) {
1984
+ const hooksData = blockingHooks.map((h) => ({
1985
+ required_tool: h.required_tool
1986
+ }));
1987
+ const constants = [
1988
+ `#!/usr/bin/env node`,
1989
+ `import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs';`,
1990
+ `import { homedir } from 'os';`,
1991
+ `import { join, dirname, resolve } from 'path';`,
1992
+ `import { fileURLToPath } from 'url';`,
1993
+ ``,
1994
+ `const __filename = fileURLToPath(import.meta.url);`,
1995
+ `const __dirname = dirname(__filename);`,
1996
+ ``,
1997
+ `const PILL_NAME = ${JSON.stringify(pillName)};`,
1998
+ `const PILL_VERSION = ${JSON.stringify(version)};`,
1999
+ `const HAS_BLOCKING_HOOKS = ${blockingHooks.length > 0};`,
2000
+ `const HOOKS = ${JSON.stringify(hooksData, null, 2)};`,
2001
+ `const VALID_AGENTS = ['claude-code', 'cursor', 'windsurf', 'github-copilot', 'codex'];`,
2002
+ ``,
2003
+ `const argv = process.argv.slice(2);`,
2004
+ `const command = argv[0];`,
2005
+ `const agentIdx = argv.indexOf('--agent');`,
2006
+ `const agent = agentIdx !== -1 ? argv[agentIdx + 1] : null;`,
2007
+ `const silent = argv.includes('--silent');`,
2008
+ ``,
2009
+ `if (!['install', 'uninstall'].includes(command)) {`,
2010
+ ` process.stderr.write('[mcpill] error: expected \\'install\\' or \\'uninstall\\', got \\'' + command + '\\'\\n');`,
2011
+ ` process.exit(1);`,
2012
+ `}`,
2013
+ `if (!agent) {`,
2014
+ ` process.stderr.write('[mcpill] error: --agent <target> is required\\n');`,
2015
+ ` process.exit(1);`,
2016
+ `}`,
2017
+ `if (!VALID_AGENTS.includes(agent)) {`,
2018
+ ` process.stderr.write('[mcpill] error: unknown agent \\'' + agent + '\\'. Valid targets: ' + VALID_AGENTS.join(', ') + '\\n');`,
2019
+ ` process.exit(1);`,
2020
+ `}`,
2021
+ ``,
2022
+ `const INDEX_PATH = resolve(__dirname, 'index.js');`,
2023
+ `const GATE_PATH = resolve(__dirname, 'hooks', 'pre-tool-gate.js');`,
2024
+ `const FLAG_PATH = resolve(__dirname, 'hooks', 'post-analysis-flag.js');`,
2025
+ `const MCP_ENTRY = { command: 'node', args: [INDEX_PATH] };`,
2026
+ `const STATE_FILE = join(homedir(), '.' + PILL_NAME, 'state.json');`
2027
+ ].join("\n");
2028
+ return constants + "\n" + INSTALL_JS_BODY;
2029
+ }
2030
+ function generateDistPackageJson(pillName, version, opts) {
2031
+ const pkg2 = {
2032
+ name: pillName,
2033
+ version,
2034
+ type: "module",
2035
+ bin: { [pillName]: "install.js" },
2036
+ dependencies: {
2037
+ "mcpster": "*",
2038
+ "mcpill-runtime": "*"
2039
+ }
2040
+ };
2041
+ if (opts.postinstall) {
2042
+ pkg2.scripts = { postinstall: `node install.js --agent claude-code --silent` };
2043
+ }
2044
+ return JSON.stringify(pkg2, null, 2) + "\n";
2045
+ }
2046
+ function emitStandalonePackage(baseDir, pillName, version, toolsJsContent, doc, blockingHooks, opts = {}) {
2047
+ const distDir = join5(baseDir, ".mcpill", "dist", pillName);
2048
+ const hooksDir = join5(distDir, "hooks");
2049
+ mkdirSync3(hooksDir, { recursive: true });
2050
+ const blockingHookTools = blockingHooks.filter((h) => h.required_tool).map((h) => h.required_tool);
2051
+ writeFileSync3(join5(distDir, "package.json"), generateDistPackageJson(pillName, version, opts));
2052
+ writeFileSync3(join5(distDir, "tools.js"), toolsJsContent);
2053
+ writeFileSync3(
2054
+ join5(distDir, "index.js"),
2055
+ generateStandaloneIndexJs(pillName, version, doc.prompts, doc.resources, blockingHookTools)
2056
+ );
2057
+ writeFileSync3(join5(distDir, "install.js"), generateStandaloneInstallJs(pillName, version, blockingHooks));
2058
+ if (blockingHooks.length > 0) {
2059
+ writeFileSync3(join5(hooksDir, "pre-tool-gate.js"), generateStandaloneGateScript(pillName, blockingHooks));
2060
+ writeFileSync3(join5(hooksDir, "post-analysis-flag.js"), generateStandaloneFlagScript(pillName));
2061
+ }
2062
+ }
2063
+
1551
2064
  // src/commands/compile.ts
1552
2065
  var zodTypeNameMap = {
1553
2066
  ZodString: "string",
@@ -1597,18 +2110,18 @@ ${r.content}`;
1597
2110
  }
1598
2111
  async function runCompile(opts) {
1599
2112
  const baseDir = resolve(opts.dir ?? process.cwd());
1600
- const mcpillDir = join5(baseDir, ".mcpill");
1601
- const serverDir = join5(mcpillDir, "server");
2113
+ const mcpillDir = join6(baseDir, ".mcpill");
2114
+ const serverDir = join6(mcpillDir, "server");
1602
2115
  if (opts.toMd) {
1603
- const configPath = join5(serverDir, "mcpill.config.json");
2116
+ const configPath = join6(serverDir, "mcpill.config.json");
1604
2117
  if (!existsSync3(configPath)) {
1605
2118
  throw new Error("No pill found \u2014 run mcpill compile first");
1606
2119
  }
1607
2120
  const config = await loadConfig(serverDir);
1608
2121
  const resources = await loadResources(serverDir);
1609
- const promptsPath = join5(serverDir, "prompts.json");
2122
+ const promptsPath = join6(serverDir, "prompts.json");
1610
2123
  const prompts = JSON.parse(readFileSync4(promptsPath, "utf-8"));
1611
- const toolsPath = join5(serverDir, "tools.js");
2124
+ const toolsPath = join6(serverDir, "tools.js");
1612
2125
  const handlers = existsSync3(toolsPath) ? extractHandlers(readFileSync4(toolsPath, "utf-8")) : /* @__PURE__ */ new Map();
1613
2126
  let tools = [];
1614
2127
  if (existsSync3(toolsPath)) {
@@ -1642,11 +2155,13 @@ async function runCompile(opts) {
1642
2155
  return;
1643
2156
  }
1644
2157
  const doc = parseServerDir(mcpillDir);
1645
- const toolsJsPath = join5(serverDir, "tools.js");
2158
+ const toolsJsPath = join6(serverDir, "tools.js");
1646
2159
  const existing = existsSync3(toolsJsPath) ? extractHandlers(readFileSync4(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
1647
2160
  const { doc: mergedDoc, stubbed } = mergeHandlers(doc, existing);
1648
- const pillMdPath = join5(baseDir, "PILL.md");
2161
+ const pillMdPath = join6(baseDir, "PILL.md");
1649
2162
  let pillContent = null;
2163
+ let pillVersion = "0.1.0";
2164
+ let pillPostinstall = false;
1650
2165
  if (existsSync3(pillMdPath)) {
1651
2166
  pillContent = readFileSync4(pillMdPath, "utf-8");
1652
2167
  const serverSection = ("\n" + pillContent).split("\n## ").find((s) => s.startsWith("Server\n"));
@@ -1656,6 +2171,8 @@ async function runCompile(opts) {
1656
2171
  if (kv["transport"] === "stdio" || kv["transport"] === "http") {
1657
2172
  mergedDoc.config.transport = kv["transport"];
1658
2173
  }
2174
+ if (kv["version"]) pillVersion = kv["version"].trim();
2175
+ if (kv["postinstall"]?.trim().toLowerCase() === "true") pillPostinstall = true;
1659
2176
  }
1660
2177
  }
1661
2178
  if (opts.strict && stubbed.length > 0) {
@@ -1664,18 +2181,21 @@ async function runCompile(opts) {
1664
2181
  for (const name of stubbed) {
1665
2182
  console.warn(`\u26A0 Stub generated for tool: ${name}`);
1666
2183
  }
1667
- mkdirSync3(serverDir, { recursive: true });
1668
- writeFileSync3(join5(serverDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
1669
- writeFileSync3(join5(serverDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
1670
- writeFileSync3(join5(serverDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
1671
- writeFileSync3(toolsJsPath, generateToolsJs(mergedDoc.tools));
2184
+ mkdirSync4(serverDir, { recursive: true });
2185
+ writeFileSync4(join6(serverDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
2186
+ writeFileSync4(join6(serverDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
2187
+ writeFileSync4(join6(serverDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
2188
+ const toolsJsContent = generateToolsJs(mergedDoc.tools);
2189
+ writeFileSync4(toolsJsPath, toolsJsContent);
1672
2190
  ensureDeps(serverDir);
1673
2191
  console.log(`\u2713 .mcpill/server/ updated from .mcpill/server.md, .mcpill/server/tools/, .mcpill/server/prompts/`);
1674
2192
  if (opts.noHooks) {
1675
- writeFileSync3(join5(mcpillDir, ".no-hooks"), "");
2193
+ writeFileSync4(join6(mcpillDir, ".no-hooks"), "");
2194
+ emitStandalonePackage(baseDir, mergedDoc.config.name, pillVersion, toolsJsContent, mergedDoc, [], { postinstall: false });
2195
+ console.log(`\u2713 .mcpill/dist/${mergedDoc.config.name}/ updated`);
1676
2196
  return;
1677
2197
  }
1678
- const agentMdPath = existsSync3(join5(baseDir, "AGENT.md")) ? join5(baseDir, "AGENT.md") : existsSync3(join5(mcpillDir, "AGENT.md")) ? join5(mcpillDir, "AGENT.md") : null;
2198
+ const agentMdPath = existsSync3(join6(baseDir, "AGENT.md")) ? join6(baseDir, "AGENT.md") : existsSync3(join6(mcpillDir, "AGENT.md")) ? join6(mcpillDir, "AGENT.md") : null;
1679
2199
  const pillTools = pillContent ? parsePillMdTools(pillContent) : [];
1680
2200
  const agentTools = agentMdPath ? parseAgentMdTools(readFileSync4(agentMdPath, "utf-8")) : [];
1681
2201
  const toolMap = new Map([...pillTools, ...agentTools].map((t) => [t.name, t]));
@@ -1686,7 +2206,7 @@ async function runCompile(opts) {
1686
2206
  console.log(`\u2713 .claude/settings.json updated with PreToolUse hook`);
1687
2207
  }
1688
2208
  const pillHooks = pillContent ? parsePillMdHooks(pillContent) : [];
1689
- const hooksDir = join5(mcpillDir, "server", "hooks");
2209
+ const hooksDir = join6(mcpillDir, "server", "hooks");
1690
2210
  const fileHooks = [];
1691
2211
  if (existsSync3(hooksDir)) {
1692
2212
  let hookFiles = [];
@@ -1695,13 +2215,29 @@ async function runCompile(opts) {
1695
2215
  } catch {
1696
2216
  }
1697
2217
  for (const file of hookFiles) {
1698
- const entry = parseHookFile(readFileSync4(join5(hooksDir, file), "utf-8"));
2218
+ const entry = parseHookFile(readFileSync4(join6(hooksDir, file), "utf-8"));
1699
2219
  if (entry) fileHooks.push(entry);
1700
2220
  }
1701
2221
  }
1702
2222
  const allUserHooks = [...pillHooks, ...fileHooks];
1703
- const blockingHooks = allUserHooks.filter((h) => h.blocking);
1704
- const regularHooks = allUserHooks.filter((h) => !h.blocking);
2223
+ const normalizedUserHooks = allUserHooks.map((h) => {
2224
+ if (h.event !== "UserPromptSubmit") return h;
2225
+ const norm = { ...h };
2226
+ if (norm.blocking) {
2227
+ console.warn(`warn: UserPromptSubmit hooks do not support blocking \u2014 field ignored`);
2228
+ delete norm.blocking;
2229
+ delete norm.required_tool;
2230
+ delete norm.gate_window_seconds;
2231
+ delete norm.instructions;
2232
+ }
2233
+ if (norm.matcher !== void 0) {
2234
+ console.warn(`warn: UserPromptSubmit hooks do not support matcher \u2014 field ignored`);
2235
+ delete norm.matcher;
2236
+ }
2237
+ return norm;
2238
+ });
2239
+ const blockingHooks = normalizedUserHooks.filter((h) => h.blocking);
2240
+ const regularHooks = normalizedUserHooks.filter((h) => !h.blocking);
1705
2241
  for (const hook of blockingHooks) {
1706
2242
  if (!hook.name) throw new Error(`blocking hook is missing a name`);
1707
2243
  if (!hook.required_tool) throw new Error(`blocking hook '${hook.name}' is missing required_tool`);
@@ -1721,6 +2257,8 @@ async function runCompile(opts) {
1721
2257
  mergeUserHooksIntoSettings(baseDir, generatedEntries);
1722
2258
  console.log(`\u2713 .claude/settings.json updated with ${blockingHooks.length} blocking hook(s)`);
1723
2259
  }
2260
+ emitStandalonePackage(baseDir, mergedDoc.config.name, pillVersion, toolsJsContent, mergedDoc, blockingHooks, { postinstall: pillPostinstall });
2261
+ console.log(`\u2713 .mcpill/dist/${mergedDoc.config.name}/ updated`);
1724
2262
  }
1725
2263
 
1726
2264
  // src/commands/pack.ts
@@ -1769,7 +2307,7 @@ async function runPublish(dir, access) {
1769
2307
  // src/cli.ts
1770
2308
  var pkgDir = dirname(fileURLToPath(import.meta.url));
1771
2309
  var pkg = JSON.parse(
1772
- readFileSync5(join6(pkgDir, "../package.json"), "utf-8")
2310
+ readFileSync5(join7(pkgDir, "../package.json"), "utf-8")
1773
2311
  );
1774
2312
  var program = new Command();
1775
2313
  program.name("mcpill").version(pkg.version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpill",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "type": "module",
5
5
  "description": "CLI for building, validating, and publishing MCP servers using the pill format.",
6
6
  "homepage": "https://mcpill.ruco.dev",