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