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 +17 -0
- package/README.md +19 -0
- package/dist/cli.js +715 -30
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
985
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
|
1228
|
-
import { readFileSync as readFileSync4, writeFileSync as
|
|
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 =
|
|
1450
|
-
const serverDir =
|
|
2108
|
+
const mcpillDir = join6(baseDir, ".mcpill");
|
|
2109
|
+
const serverDir = join6(mcpillDir, "server");
|
|
1451
2110
|
if (opts.toMd) {
|
|
1452
|
-
const configPath =
|
|
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 =
|
|
2117
|
+
const promptsPath = join6(serverDir, "prompts.json");
|
|
1459
2118
|
const prompts = JSON.parse(readFileSync4(promptsPath, "utf-8"));
|
|
1460
|
-
const toolsPath =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
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(
|
|
2289
|
+
readFileSync5(join7(pkgDir, "../package.json"), "utf-8")
|
|
1605
2290
|
);
|
|
1606
2291
|
var program = new Command();
|
|
1607
2292
|
program.name("mcpill").version(pkg.version);
|