@privacyscrubber/mcp-server 2.0.3 → 2.2.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.
@@ -9,7 +9,7 @@
9
9
  * Usage: pii-masking-run [options] -- <command> [args...]
10
10
  */
11
11
 
12
- import { spawn } from 'child_process';
12
+ import { spawn, execSync } from 'child_process';
13
13
  import { createRequire } from 'module';
14
14
  import fs from 'fs';
15
15
  import path from 'path';
@@ -27,24 +27,39 @@ const cmdIndex = args.indexOf("--");
27
27
 
28
28
  if (args.includes("--version") || args.includes("-v")) {
29
29
  const pkgPath = path.resolve(__dirname, './package.json');
30
- let version = '1.7.0';
30
+ let version = '2.2.0';
31
31
  try { version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch (_) {}
32
- console.log(`pii-masking-run v${version} (PrivacyScrubber MCP)`);
32
+ console.log(`ps-guard / pii-masking-run v${version} (PrivacyScrubber MCP)`);
33
33
  process.exit(0);
34
34
  }
35
35
 
36
- if (cmdIndex === -1 || args.includes("-h") || args.includes("--help")) {
37
- console.log("PrivacyScrubber CLI Command Runner: pii-masking-run");
36
+ const isDiffMode = args.includes("--diff");
37
+ const isRulesMode = args.includes("--rules");
38
+ const hasCommand = cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0;
39
+ const hasStdin = !process.stdin.isTTY;
40
+
41
+ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !hasStdin)) {
42
+ console.log("PrivacyScrubber CLI & Agentic Guard: ps-guard / pii-masking-run");
38
43
  console.log("\nUsage:");
39
- console.log(" npx pii-masking-run [options] -- <command> [args...]");
44
+ console.log(" npx ps-guard [options] -- <command> [args...]");
45
+ console.log(" cat <file> | npx ps-guard [options]");
46
+ console.log(" npx ps-guard --diff [--staged] [options]");
47
+ console.log(" npx ps-guard --rules [targets]");
40
48
  console.log("\nOptions:");
41
- console.log(" --profile <name> Specify detection profile (e.g. General, Medical, Legal, Dev, HR)");
49
+ console.log(" --profile <name> Specify detection profile (e.g. General, Dev, Medical, Legal, HR)");
42
50
  console.log(" --key <licKey> PrivacyScrubber PRO License Key");
51
+ console.log(" --diff Sanitize git diff output before committing or sending to AI");
52
+ console.log(" --staged Use staged git changes with --diff");
53
+ console.log(" --rules [targets] Generate AI agent rules (.cursorrules, .windsurfrules, CLAUDE.md, copilot, cline)");
54
+ console.log(" -q, --quiet Suppress stderr proof telemetry");
43
55
  console.log(" -v, --version Print version");
44
56
  console.log(" -h, --help Show this help screen");
45
57
  console.log("\nExamples:");
46
- console.log(" npx pii-masking-run -- cat database-dump.sql");
47
- console.log(" npx pii-masking-run --profile dev -- npm test");
58
+ console.log(" npx ps-guard -- cat database-dump.sql");
59
+ console.log(" npx ps-guard --profile dev -- npm test");
60
+ console.log(" cat .env | npx ps-guard --profile dev");
61
+ console.log(" npx ps-guard --diff --staged");
62
+ console.log(" npx ps-guard --rules cursor,windsurf");
48
63
  process.exit(0);
49
64
  }
50
65
 
@@ -126,14 +141,9 @@ function loadCustomRules() {
126
141
  // Local runtime variables
127
142
  let profile = "General";
128
143
  let key = process.env.PRIVACYSCRUBBER_KEY || "";
144
+ let rulesTargets = ["all"];
129
145
 
130
- const optionsArgs = args.slice(0, cmdIndex);
131
- const commandArgs = args.slice(cmdIndex + 1);
132
-
133
- if (commandArgs.length === 0) {
134
- console.error("Error: No command specified after '--'.");
135
- process.exit(1);
136
- }
146
+ const optionsArgs = cmdIndex !== -1 ? args.slice(0, cmdIndex) : args;
137
147
 
138
148
  // Parse custom command-line options
139
149
  for (let i = 0; i < optionsArgs.length; i++) {
@@ -143,6 +153,11 @@ for (let i = 0; i < optionsArgs.length; i++) {
143
153
  } else if (optionsArgs[i] === "--key" && optionsArgs[i + 1]) {
144
154
  key = optionsArgs[i + 1];
145
155
  i++;
156
+ } else if (optionsArgs[i] === "--rules") {
157
+ if (optionsArgs[i + 1] && !optionsArgs[i + 1].startsWith("-")) {
158
+ rulesTargets = optionsArgs[i + 1].split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
159
+ i++;
160
+ }
146
161
  }
147
162
  }
148
163
 
@@ -154,9 +169,26 @@ const customRules = loadCustomRules();
154
169
  const localSessionMap = {};
155
170
 
156
171
  function performSanitization(text) {
157
- const normalizedProfile = (profile || "general").trim().toLowerCase();
158
- const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, localSessionMap);
159
172
  const license = checkLicenseStatus();
173
+ const normalizedProfile = (profile || "general").trim().toLowerCase();
174
+ const limit = (normalizedProfile !== "general" && !license.isPro) ? 5000 : 15000;
175
+ let textToScrub = text;
176
+ let wasTruncated = false;
177
+ if (!license.isPro && textToScrub.length > limit) {
178
+ textToScrub = textToScrub.slice(0, limit);
179
+ wasTruncated = true;
180
+ }
181
+
182
+ let result;
183
+ try {
184
+ result = PrivacyScrubberCore.scrubText(textToScrub, customRules, {}, normalizedProfile, localSessionMap, license.isPro);
185
+ } catch (err) {
186
+ return text;
187
+ }
188
+
189
+ if (result && result.tokenMap) {
190
+ Object.assign(localSessionMap, result.tokenMap);
191
+ }
160
192
 
161
193
  if (!license.isPro && result.tokenMap) {
162
194
  // Free tier: check for secrets and warn
@@ -181,40 +213,160 @@ function performSanitization(text) {
181
213
  }
182
214
  }
183
215
 
184
- return result.scrubbedText;
216
+ return result.scrubbedText + (wasTruncated ? '\n[Output truncated to Free Tier character limit. Upgrade to PRO for unlimited.]' : '');
217
+ }
218
+
219
+ function emitProofFeedback() {
220
+ const isQuiet = args.includes("--quiet") || args.includes("-q");
221
+ if (isQuiet) return;
222
+
223
+ const totalMasked = Object.keys(localSessionMap).length;
224
+ if (totalMasked > 0) {
225
+ process.stderr.write(`\x1b[36m[ps-guard] ${totalMasked} item(s) masked · 0 bytes egress · Zero-Trust RAM verified\x1b[0m\n`);
226
+ } else if (args.includes("--verbose")) {
227
+ process.stderr.write(`\x1b[36m[ps-guard] 0 item(s) masked · 0 bytes egress · Zero-Trust RAM verified\x1b[0m\n`);
228
+ }
229
+ }
230
+
231
+ // Mode 1: Agent Rules Generation (--rules)
232
+ if (isRulesMode) {
233
+ const targetDir = process.cwd();
234
+ const targets = rulesTargets.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline"] : rulesTargets;
235
+ const RULE_TEXT = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
236
+ # 100% Local In-Memory Data Sanitization. Zero Network Egress.
237
+
238
+ ## Agent Mandate
239
+ You are an autonomous AI assistant operating under strict Zero-Trust Data Sanitization (ZTDS) policies.
240
+ Before reading sensitive files, running terminal commands that may print credentials, or committing code:
241
+
242
+ 1. **Terminal Commands:** When running shell commands whose output may contain API keys, database connection strings, or customer PII (e.g. \`cat .env\`, \`docker logs\`, \`env\`, \`git diff\`), invoke the \`guard_exec\` tool instead of raw terminal execution.
243
+ 2. **Reading Credentials/Config:** When reading \`.env*\`, config files, database credentials, or secret keys, always call \`guard_read_file\` so sensitive entities are tokenized locally in RAM before entering your context window.
244
+ 3. **Git Code Review:** Use \`guard_git_diff\` to inspect changes without exposing plaintext secrets to the cloud.
245
+ 4. **Applying Patches:** When writing files containing token placeholders (e.g. \`[API_KEY_1]\`, \`[DB_PASSWORD_1]\`), use \`guard_apply_patch\` to automatically restore authentic secrets locally to disk without sending them back to the LLM.
246
+ 5. **Final Output:** Pass any final synthesized text containing token placeholders through \`reveal_text\` before presenting it to the user.
247
+ `;
248
+
249
+ const generatedFiles = [];
250
+ if (targets.includes("cursor")) {
251
+ fs.writeFileSync(path.join(targetDir, '.cursorrules'), RULE_TEXT, 'utf8');
252
+ generatedFiles.push('.cursorrules');
253
+ }
254
+ if (targets.includes("windsurf")) {
255
+ fs.writeFileSync(path.join(targetDir, '.windsurfrules'), RULE_TEXT, 'utf8');
256
+ generatedFiles.push('.windsurfrules');
257
+ }
258
+ if (targets.includes("claude_code") || targets.includes("claude")) {
259
+ const p = path.join(targetDir, 'CLAUDE.md');
260
+ if (fs.existsSync(p)) {
261
+ const existing = fs.readFileSync(p, 'utf8');
262
+ if (!existing.includes("Zero-Trust Agentic Guard")) {
263
+ fs.writeFileSync(p, existing + "\n\n" + RULE_TEXT, 'utf8');
264
+ generatedFiles.push('CLAUDE.md (appended)');
265
+ } else {
266
+ generatedFiles.push('CLAUDE.md (already present)');
267
+ }
268
+ } else {
269
+ fs.writeFileSync(p, RULE_TEXT, 'utf8');
270
+ generatedFiles.push('CLAUDE.md');
271
+ }
272
+ }
273
+ if (targets.includes("copilot")) {
274
+ const copilotDir = path.join(targetDir, '.github');
275
+ if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
276
+ fs.writeFileSync(path.join(copilotDir, 'copilot-instructions.md'), RULE_TEXT, 'utf8');
277
+ generatedFiles.push('.github/copilot-instructions.md');
278
+ }
279
+ if (targets.includes("cline")) {
280
+ fs.writeFileSync(path.join(targetDir, '.clinerules'), RULE_TEXT, 'utf8');
281
+ generatedFiles.push('.clinerules');
282
+ }
283
+
284
+ console.log(`[Zero-Trust Agentic Guard: Agent Rules Generated]`);
285
+ console.log(`Workspace: ${targetDir}`);
286
+ console.log(`Files Created/Updated:`);
287
+ generatedFiles.forEach(f => console.log(` - ${f}`));
288
+ process.exit(0);
185
289
  }
186
290
 
187
- // Spawn child process capturing output
188
- const child = spawn(commandArgs[0], commandArgs.slice(1), {
189
- stdio: ['inherit', 'pipe', 'pipe'],
190
- shell: true
191
- });
192
-
193
- const rlStdout = readline.createInterface({
194
- input: child.stdout,
195
- terminal: false
196
- });
197
-
198
- rlStdout.on('line', (line) => {
199
- const sanitized = performSanitization(line);
200
- process.stdout.write(sanitized + '\n');
201
- });
202
-
203
- const rlStderr = readline.createInterface({
204
- input: child.stderr,
205
- terminal: false
206
- });
207
-
208
- rlStderr.on('line', (line) => {
209
- const sanitized = performSanitization(line);
210
- process.stderr.write(sanitized + '\n');
211
- });
212
-
213
- child.on('close', (code) => {
214
- process.exit(code || 0);
215
- });
216
-
217
- child.on('error', (err) => {
218
- console.error(`Error spawning command: ${err.message}`);
219
- process.exit(1);
220
- });
291
+ // Mode 2: Git Diff Mode (--diff)
292
+ if (isDiffMode) {
293
+ const isStaged = args.includes("--staged") || args.includes("--cached");
294
+ const gitCmd = isStaged ? "git diff --cached" : "git diff";
295
+ try {
296
+ let diffOutput = '';
297
+ try {
298
+ diffOutput = execSync(gitCmd, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
299
+ } catch (err) {
300
+ if (err.code === 'ENOBUFS' && err.stdout) {
301
+ diffOutput = err.stdout.toString('utf8');
302
+ } else {
303
+ throw err;
304
+ }
305
+ }
306
+ if (!diffOutput.trim()) {
307
+ console.log(`[Zero-Trust Agentic Guard: Git Diff]\nNo git diff changes detected (${isStaged ? 'staged' : 'unstaged'}).`);
308
+ process.exit(0);
309
+ }
310
+ const sanitizedDiff = performSanitization(diffOutput);
311
+ process.stdout.write(sanitizedDiff.endsWith('\n') ? sanitizedDiff : sanitizedDiff + '\n');
312
+ emitProofFeedback();
313
+ process.exit(0);
314
+ } catch (err) {
315
+ console.error(`Error running git diff: ${err.message}`);
316
+ process.exit(1);
317
+ }
318
+ }
319
+
320
+ // Mode 3: Stdin Stream Mode (pipe)
321
+ if (hasStdin && !hasCommand) {
322
+ const rlStdin = readline.createInterface({
323
+ input: process.stdin,
324
+ terminal: false
325
+ });
326
+ rlStdin.on('line', (line) => {
327
+ const sanitized = performSanitization(line);
328
+ process.stdout.write(sanitized + '\n');
329
+ });
330
+ rlStdin.on('close', () => {
331
+ emitProofFeedback();
332
+ process.exit(0);
333
+ });
334
+ } else if (hasCommand) {
335
+ // Mode 4: Spawn Child Process (-- <command>)
336
+ const commandArgs = args.slice(cmdIndex + 1);
337
+ const fullCommand = commandArgs.join(' ');
338
+ const child = spawn(fullCommand, {
339
+ stdio: ['inherit', 'pipe', 'pipe'],
340
+ shell: true
341
+ });
342
+
343
+ const rlStdout = readline.createInterface({
344
+ input: child.stdout,
345
+ terminal: false
346
+ });
347
+
348
+ rlStdout.on('line', (line) => {
349
+ const sanitized = performSanitization(line);
350
+ process.stdout.write(sanitized + '\n');
351
+ });
352
+
353
+ const rlStderr = readline.createInterface({
354
+ input: child.stderr,
355
+ terminal: false
356
+ });
357
+
358
+ rlStderr.on('line', (line) => {
359
+ const sanitized = performSanitization(line);
360
+ process.stderr.write(sanitized + '\n');
361
+ });
362
+
363
+ child.on('close', (code) => {
364
+ emitProofFeedback();
365
+ process.exit(code || 0);
366
+ });
367
+
368
+ child.on('error', (err) => {
369
+ console.error(`Error spawning command: ${err.message}`);
370
+ process.exit(1);
371
+ });
372
+ }