mcpill 1.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
14
+ ## 1.9.0
15
+
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
17
+ - `required_tool`, `gate_window_seconds` (default 120 s), and multiline `instructions` fields supported on blocking hooks
18
+ - Compile-time validation: `required_tool` must name a known tool; error surfaced during `mcpill compile`
19
+
3
20
  ## 1.8.0
4
21
 
5
22
  - `mcpill run` (stdio) now auto-registers the server in Claude config on start and auto-unregisters on SIGINT, SIGTERM, and process exit — no manual step required
package/README.md CHANGED
@@ -78,6 +78,25 @@ Compile also writes a `PreToolUse` hook into `.claude/settings.json` that warns
78
78
 
79
79
  - **`AGENT.md`** — a `## Tools` table with a `Replaces` column (existing format). AGENT.md wins on name conflicts.
80
80
 
81
+ #### Blocking hooks
82
+
83
+ For mandatory workflow steps, add a `## Hook:` section to `PILL.md` with `blocking: true`. Compile generates a hard-enforcement gate instead of a soft reminder:
84
+
85
+ ```markdown
86
+ ## Hook: my-hook
87
+
88
+ trigger: PreToolUse
89
+ blocking: true
90
+ required_tool: analyze-english
91
+ gate_window_seconds: 120
92
+ instructions: |
93
+ Before calling any other tool, call mcp__my-pill__analyze-english first.
94
+ ```
95
+
96
+ At runtime the gate script exits 1 (blocking the tool call) until `mcp__{pill}__analyze-english` has run within the window. After it runs, the flag script records the timestamp and the gate passes for `gate_window_seconds` seconds (default 120). Pill's own MCP tools are always allowed through. The gate is fail-open — if the pill's state file is missing, the gate passes.
97
+
98
+ `required_tool` must name a tool defined in the same pill; `mcpill compile` aborts with an error if it doesn't.
99
+
81
100
  ### `mcpill validate`
82
101
 
83
102
  Validates all pill directories (`.<name>/`) in the project root.
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
@@ -756,12 +756,45 @@ function extractFencedBlock(body, lang) {
756
756
  }
757
757
  function parseKvBlock(body) {
758
758
  const result = {};
759
- for (const line of body.split("\n")) {
759
+ const lines = body.split("\n");
760
+ let i = 0;
761
+ while (i < lines.length) {
762
+ const line = lines[i];
760
763
  const colonIdx = line.indexOf(":");
761
- if (colonIdx === -1) continue;
764
+ if (colonIdx === -1) {
765
+ i++;
766
+ continue;
767
+ }
762
768
  const key = line.slice(0, colonIdx).trim();
763
769
  const value = line.slice(colonIdx + 1).trim();
764
- if (key) result[key] = value;
770
+ if (!key) {
771
+ i++;
772
+ continue;
773
+ }
774
+ if (value === "|") {
775
+ i++;
776
+ const blockLines = [];
777
+ let baseIndent = -1;
778
+ while (i < lines.length) {
779
+ const nextLine = lines[i];
780
+ const trimmed = nextLine.trimStart();
781
+ if (trimmed === "") {
782
+ blockLines.push("");
783
+ i++;
784
+ continue;
785
+ }
786
+ const indent = nextLine.length - trimmed.length;
787
+ if (baseIndent === -1) baseIndent = indent;
788
+ if (indent < baseIndent) break;
789
+ blockLines.push(nextLine.slice(baseIndent));
790
+ i++;
791
+ }
792
+ while (blockLines.length > 0 && blockLines[blockLines.length - 1] === "") blockLines.pop();
793
+ result[key] = blockLines.join("\n");
794
+ } else {
795
+ result[key] = value;
796
+ i++;
797
+ }
765
798
  }
766
799
  return result;
767
800
  }
@@ -976,13 +1009,31 @@ function parsePillMdHooks(content) {
976
1009
  for (const piece of pieces) {
977
1010
  if (!piece.toLowerCase().startsWith("hook:")) continue;
978
1011
  const nlIdx = piece.indexOf("\n");
1012
+ const sectionTitle = (nlIdx === -1 ? piece : piece.slice(0, nlIdx)).trim();
1013
+ const hookName = sectionTitle.slice("hook:".length).trim();
979
1014
  const body = nlIdx === -1 ? "" : piece.slice(nlIdx + 1);
980
1015
  const kv = parseKvBlock(body);
981
1016
  const trigger = kv["trigger"]?.trim();
982
1017
  const matcher = kv["matcher"]?.trim();
983
1018
  const command = kv["command"]?.trim();
984
- if (!trigger || !command) continue;
985
- hooks.push({ event: trigger, ...matcher ? { matcher } : {}, command });
1019
+ const blocking = kv["blocking"]?.trim().toLowerCase() === "true";
1020
+ if (!trigger || !command && !blocking) continue;
1021
+ const entry = {
1022
+ ...hookName ? { name: hookName } : {},
1023
+ event: trigger,
1024
+ ...matcher ? { matcher } : {},
1025
+ ...command ? { command } : {},
1026
+ ...blocking ? { blocking: true } : {}
1027
+ };
1028
+ if (blocking) {
1029
+ const requiredTool = kv["required_tool"]?.trim();
1030
+ if (requiredTool) entry.required_tool = requiredTool;
1031
+ const windowRaw = kv["gate_window_seconds"]?.trim();
1032
+ if (windowRaw) entry.gate_window_seconds = parseInt(windowRaw, 10);
1033
+ const instructions = kv["instructions"]?.trim();
1034
+ if (instructions) entry.instructions = instructions;
1035
+ }
1036
+ hooks.push(entry);
986
1037
  }
987
1038
  return hooks;
988
1039
  }
@@ -992,8 +1043,23 @@ function parseHookFile(content) {
992
1043
  const trigger = kv["trigger"]?.trim();
993
1044
  const matcher = kv["matcher"]?.trim();
994
1045
  const command = kv["command"]?.trim();
995
- if (!trigger || !command) return null;
996
- return { event: trigger, ...matcher ? { matcher } : {}, command };
1046
+ const blocking = kv["blocking"]?.trim().toLowerCase() === "true";
1047
+ if (!trigger || !command && !blocking) return null;
1048
+ const entry = {
1049
+ event: trigger,
1050
+ ...matcher ? { matcher } : {},
1051
+ ...command ? { command } : {},
1052
+ ...blocking ? { blocking: true } : {}
1053
+ };
1054
+ if (blocking) {
1055
+ const requiredTool = kv["required_tool"]?.trim();
1056
+ if (requiredTool) entry.required_tool = requiredTool;
1057
+ const windowRaw = kv["gate_window_seconds"]?.trim();
1058
+ if (windowRaw) entry.gate_window_seconds = parseInt(windowRaw, 10);
1059
+ const instructions = kv["instructions"]?.trim();
1060
+ if (instructions) entry.instructions = instructions;
1061
+ }
1062
+ return entry;
997
1063
  }
998
1064
  function mergeUserHooksIntoSettings(baseDir, hookBlocks) {
999
1065
  if (hookBlocks.length === 0) return;
@@ -1040,6 +1106,91 @@ function mergeHookIntoSettings(baseDir, hookEntry) {
1040
1106
  mkdirSync(claudeDir, { recursive: true });
1041
1107
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1042
1108
  }
1109
+ function generateGateScript(pillName, requiredTool, windowSeconds, instructions) {
1110
+ const instrJson = JSON.stringify(instructions);
1111
+ return [
1112
+ `import { readFileSync, existsSync } from 'fs';`,
1113
+ `import { homedir } from 'os';`,
1114
+ `import { join } from 'path';`,
1115
+ ``,
1116
+ `const PILL_NAME = ${JSON.stringify(pillName)};`,
1117
+ `const REQUIRED_TOOL = ${JSON.stringify(requiredTool)};`,
1118
+ `const GATE_WINDOW_SECONDS = ${windowSeconds};`,
1119
+ `const INSTRUCTIONS = ${instrJson};`,
1120
+ ``,
1121
+ `const raw = readFileSync(0, 'utf-8');`,
1122
+ `const input = JSON.parse(raw);`,
1123
+ `const toolName = input.tool_name ?? '';`,
1124
+ ``,
1125
+ `if (toolName.startsWith(\`mcp__\${PILL_NAME}__\`)) process.exit(0);`,
1126
+ ``,
1127
+ `const stateFile = join(homedir(), \`.\${PILL_NAME}\`, 'state.json');`,
1128
+ `if (!existsSync(stateFile)) process.exit(0);`,
1129
+ ``,
1130
+ `const state = JSON.parse(readFileSync(stateFile, 'utf-8'));`,
1131
+ `if (state.enabled === false) process.exit(0);`,
1132
+ ``,
1133
+ `const flagFile = join(homedir(), \`.\${PILL_NAME}\`, \`last_\${REQUIRED_TOOL}_run.json\`);`,
1134
+ `if (existsSync(flagFile)) {`,
1135
+ ` const flag = JSON.parse(readFileSync(flagFile, 'utf-8'));`,
1136
+ ` const elapsedMs = Date.now() - new Date(flag.timestamp).getTime();`,
1137
+ ` if (elapsedMs <= GATE_WINDOW_SECONDS * 1000) process.exit(0);`,
1138
+ `}`,
1139
+ ``,
1140
+ `process.stderr.write(INSTRUCTIONS + '\\n');`,
1141
+ `process.exit(1);`,
1142
+ ``
1143
+ ].join("\n");
1144
+ }
1145
+ function generateFlagScript(pillName, requiredTool) {
1146
+ return [
1147
+ `import { writeFileSync, mkdirSync } from 'fs';`,
1148
+ `import { homedir } from 'os';`,
1149
+ `import { join } from 'path';`,
1150
+ ``,
1151
+ `const PILL_NAME = ${JSON.stringify(pillName)};`,
1152
+ `const REQUIRED_TOOL = ${JSON.stringify(requiredTool)};`,
1153
+ ``,
1154
+ `const dir = join(homedir(), \`.\${PILL_NAME}\`);`,
1155
+ `const flagFile = join(dir, \`last_\${REQUIRED_TOOL}_run.json\`);`,
1156
+ ``,
1157
+ `try {`,
1158
+ ` mkdirSync(dir, { recursive: true });`,
1159
+ ` writeFileSync(flagFile, JSON.stringify({ timestamp: new Date().toISOString() }));`,
1160
+ `} catch (e) {`,
1161
+ ` process.stderr.write(\`[\${PILL_NAME}] warning: could not write flag file: \${e.message}\\n\`);`,
1162
+ `}`,
1163
+ ``
1164
+ ].join("\n");
1165
+ }
1166
+ function applyBlockingHookScripts(baseDir, pillName, hook) {
1167
+ const hooksDir = join3(baseDir, ".mcpill", "server", "hooks");
1168
+ mkdirSync(hooksDir, { recursive: true });
1169
+ const hookName = hook.name;
1170
+ const requiredTool = hook.required_tool;
1171
+ const windowSeconds = hook.gate_window_seconds ?? 120;
1172
+ const instructions = hook.instructions ?? `[mcpill:${pillName}] Run mcp__${pillName}__${requiredTool} before calling other tools.`;
1173
+ writeFileSync(
1174
+ join3(hooksDir, `${hookName}-gate.mjs`),
1175
+ generateGateScript(pillName, requiredTool, windowSeconds, instructions)
1176
+ );
1177
+ writeFileSync(
1178
+ join3(hooksDir, `${hookName}-flag.mjs`),
1179
+ generateFlagScript(pillName, requiredTool)
1180
+ );
1181
+ return [
1182
+ {
1183
+ event: "PreToolUse",
1184
+ matcher: ".*",
1185
+ command: `node .mcpill/server/hooks/${hookName}-gate.mjs`
1186
+ },
1187
+ {
1188
+ event: "PostToolUse",
1189
+ matcher: `mcp__${pillName}__${requiredTool}`,
1190
+ command: `node .mcpill/server/hooks/${hookName}-flag.mjs`
1191
+ }
1192
+ ];
1193
+ }
1043
1194
 
1044
1195
  // src/commands/validate.ts
1045
1196
  async function validateOne(mcpillDir) {
@@ -1224,8 +1375,8 @@ async function runServer(opts) {
1224
1375
  }
1225
1376
 
1226
1377
  // src/commands/compile.ts
1227
- import { resolve, join as join5 } from "path";
1228
- 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";
1229
1380
  import { pathToFileURL as pathToFileURL2 } from "url";
1230
1381
 
1231
1382
  // src/compiler/serialize.ts
@@ -1397,6 +1548,514 @@ function mergeHandlers(doc, existing) {
1397
1548
  return { doc: { ...doc, tools }, stubbed };
1398
1549
  }
1399
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
+
1400
2059
  // src/commands/compile.ts
1401
2060
  var zodTypeNameMap = {
1402
2061
  ZodString: "string",
@@ -1446,18 +2105,18 @@ ${r.content}`;
1446
2105
  }
1447
2106
  async function runCompile(opts) {
1448
2107
  const baseDir = resolve(opts.dir ?? process.cwd());
1449
- const mcpillDir = join5(baseDir, ".mcpill");
1450
- const serverDir = join5(mcpillDir, "server");
2108
+ const mcpillDir = join6(baseDir, ".mcpill");
2109
+ const serverDir = join6(mcpillDir, "server");
1451
2110
  if (opts.toMd) {
1452
- const configPath = join5(serverDir, "mcpill.config.json");
2111
+ const configPath = join6(serverDir, "mcpill.config.json");
1453
2112
  if (!existsSync3(configPath)) {
1454
2113
  throw new Error("No pill found \u2014 run mcpill compile first");
1455
2114
  }
1456
2115
  const config = await loadConfig(serverDir);
1457
2116
  const resources = await loadResources(serverDir);
1458
- const promptsPath = join5(serverDir, "prompts.json");
2117
+ const promptsPath = join6(serverDir, "prompts.json");
1459
2118
  const prompts = JSON.parse(readFileSync4(promptsPath, "utf-8"));
1460
- const toolsPath = join5(serverDir, "tools.js");
2119
+ const toolsPath = join6(serverDir, "tools.js");
1461
2120
  const handlers = existsSync3(toolsPath) ? extractHandlers(readFileSync4(toolsPath, "utf-8")) : /* @__PURE__ */ new Map();
1462
2121
  let tools = [];
1463
2122
  if (existsSync3(toolsPath)) {
@@ -1491,11 +2150,13 @@ async function runCompile(opts) {
1491
2150
  return;
1492
2151
  }
1493
2152
  const doc = parseServerDir(mcpillDir);
1494
- const toolsJsPath = join5(serverDir, "tools.js");
2153
+ const toolsJsPath = join6(serverDir, "tools.js");
1495
2154
  const existing = existsSync3(toolsJsPath) ? extractHandlers(readFileSync4(toolsJsPath, "utf-8")) : /* @__PURE__ */ new Map();
1496
2155
  const { doc: mergedDoc, stubbed } = mergeHandlers(doc, existing);
1497
- const pillMdPath = join5(baseDir, "PILL.md");
2156
+ const pillMdPath = join6(baseDir, "PILL.md");
1498
2157
  let pillContent = null;
2158
+ let pillVersion = "0.1.0";
2159
+ let pillPostinstall = false;
1499
2160
  if (existsSync3(pillMdPath)) {
1500
2161
  pillContent = readFileSync4(pillMdPath, "utf-8");
1501
2162
  const serverSection = ("\n" + pillContent).split("\n## ").find((s) => s.startsWith("Server\n"));
@@ -1505,6 +2166,8 @@ async function runCompile(opts) {
1505
2166
  if (kv["transport"] === "stdio" || kv["transport"] === "http") {
1506
2167
  mergedDoc.config.transport = kv["transport"];
1507
2168
  }
2169
+ if (kv["version"]) pillVersion = kv["version"].trim();
2170
+ if (kv["postinstall"]?.trim().toLowerCase() === "true") pillPostinstall = true;
1508
2171
  }
1509
2172
  }
1510
2173
  if (opts.strict && stubbed.length > 0) {
@@ -1513,18 +2176,21 @@ async function runCompile(opts) {
1513
2176
  for (const name of stubbed) {
1514
2177
  console.warn(`\u26A0 Stub generated for tool: ${name}`);
1515
2178
  }
1516
- mkdirSync3(serverDir, { recursive: true });
1517
- writeFileSync3(join5(serverDir, "mcpill.config.json"), JSON.stringify(mergedDoc.config, null, 2));
1518
- writeFileSync3(join5(serverDir, "prompts.json"), JSON.stringify(mergedDoc.prompts, null, 2));
1519
- writeFileSync3(join5(serverDir, "resources.md"), serializeResourcesMd(mergedDoc.resources));
1520
- 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);
1521
2185
  ensureDeps(serverDir);
1522
2186
  console.log(`\u2713 .mcpill/server/ updated from .mcpill/server.md, .mcpill/server/tools/, .mcpill/server/prompts/`);
1523
2187
  if (opts.noHooks) {
1524
- 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`);
1525
2191
  return;
1526
2192
  }
1527
- 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;
1528
2194
  const pillTools = pillContent ? parsePillMdTools(pillContent) : [];
1529
2195
  const agentTools = agentMdPath ? parseAgentMdTools(readFileSync4(agentMdPath, "utf-8")) : [];
1530
2196
  const toolMap = new Map([...pillTools, ...agentTools].map((t) => [t.name, t]));
@@ -1535,7 +2201,7 @@ async function runCompile(opts) {
1535
2201
  console.log(`\u2713 .claude/settings.json updated with PreToolUse hook`);
1536
2202
  }
1537
2203
  const pillHooks = pillContent ? parsePillMdHooks(pillContent) : [];
1538
- const hooksDir = join5(mcpillDir, "server", "hooks");
2204
+ const hooksDir = join6(mcpillDir, "server", "hooks");
1539
2205
  const fileHooks = [];
1540
2206
  if (existsSync3(hooksDir)) {
1541
2207
  let hookFiles = [];
@@ -1544,15 +2210,34 @@ async function runCompile(opts) {
1544
2210
  } catch {
1545
2211
  }
1546
2212
  for (const file of hookFiles) {
1547
- const entry = parseHookFile(readFileSync4(join5(hooksDir, file), "utf-8"));
2213
+ const entry = parseHookFile(readFileSync4(join6(hooksDir, file), "utf-8"));
1548
2214
  if (entry) fileHooks.push(entry);
1549
2215
  }
1550
2216
  }
1551
2217
  const allUserHooks = [...pillHooks, ...fileHooks];
1552
- if (allUserHooks.length > 0) {
1553
- mergeUserHooksIntoSettings(baseDir, allUserHooks);
1554
- console.log(`\u2713 .claude/settings.json updated with ${allUserHooks.length} user-defined hook(s)`);
2218
+ const blockingHooks = allUserHooks.filter((h) => h.blocking);
2219
+ const regularHooks = allUserHooks.filter((h) => !h.blocking);
2220
+ for (const hook of blockingHooks) {
2221
+ if (!hook.name) throw new Error(`blocking hook is missing a name`);
2222
+ if (!hook.required_tool) throw new Error(`blocking hook '${hook.name}' is missing required_tool`);
2223
+ const toolExists = mergedDoc.tools.some((t) => t.name === hook.required_tool);
2224
+ if (!toolExists) {
2225
+ throw new Error(`blocking hook '${hook.name}' references unknown tool '${hook.required_tool}'`);
2226
+ }
2227
+ }
2228
+ if (regularHooks.length > 0) {
2229
+ mergeUserHooksIntoSettings(baseDir, regularHooks);
2230
+ console.log(`\u2713 .claude/settings.json updated with ${regularHooks.length} user-defined hook(s)`);
2231
+ }
2232
+ if (blockingHooks.length > 0) {
2233
+ const generatedEntries = blockingHooks.flatMap(
2234
+ (hook) => applyBlockingHookScripts(baseDir, mergedDoc.config.name, hook)
2235
+ );
2236
+ mergeUserHooksIntoSettings(baseDir, generatedEntries);
2237
+ console.log(`\u2713 .claude/settings.json updated with ${blockingHooks.length} blocking hook(s)`);
1555
2238
  }
2239
+ emitStandalonePackage(baseDir, mergedDoc.config.name, pillVersion, toolsJsContent, mergedDoc, blockingHooks, { postinstall: pillPostinstall });
2240
+ console.log(`\u2713 .mcpill/dist/${mergedDoc.config.name}/ updated`);
1556
2241
  }
1557
2242
 
1558
2243
  // src/commands/pack.ts
@@ -1601,7 +2286,7 @@ async function runPublish(dir, access) {
1601
2286
  // src/cli.ts
1602
2287
  var pkgDir = dirname(fileURLToPath(import.meta.url));
1603
2288
  var pkg = JSON.parse(
1604
- readFileSync5(join6(pkgDir, "../package.json"), "utf-8")
2289
+ readFileSync5(join7(pkgDir, "../package.json"), "utf-8")
1605
2290
  );
1606
2291
  var program = new Command();
1607
2292
  program.name("mcpill").version(pkg.version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpill",
3
- "version": "1.8.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",