@privacyscrubber/mcp-server 2.1.0 β†’ 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.
package/README.md CHANGED
@@ -96,6 +96,28 @@ Add this to your Claude Desktop config file:
96
96
  * **Command:** `npx -y @privacyscrubber/mcp-server`
97
97
  3. Optional: Set `PRIVACYSCRUBBER_KEY` as an environment variable in your system shell.
98
98
 
99
+ ### Cline / Roo Code
100
+ Add to `cline_mcp_settings.json`:
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "privacyscrubber": {
105
+ "command": "npx",
106
+ "args": ["-y", "@privacyscrubber/mcp-server"],
107
+ "env": {
108
+ "PRIVACYSCRUBBER_KEY": "YOUR_OPTIONAL_PRO_LICENSE_KEY"
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### Claude Code CLI
116
+ Add directly from your terminal:
117
+ ```bash
118
+ claude mcp add privacyscrubber -- npx -y @privacyscrubber/mcp-server
119
+ ```
120
+
99
121
  ---
100
122
 
101
123
  ## πŸ› οΈ Provided Tools & JSON-RPC Specifications
@@ -167,35 +189,47 @@ Reads a local file, extracts text, sanitizes it, and returns the redacted templa
167
189
  * `filePath` (string, required): Absolute file path to read and sanitize.
168
190
  * `profile` (string, optional): The industry detection profile.
169
191
 
170
- ---
192
+ ### 4. `guard_exec` (Command Execution Firewall)
193
+ Safely executes terminal commands in an isolated child process, masking stdout/stderr PII, database credentials, and API keys in local RAM before passing them to the AI agent. Includes a CISO audit receipt in stderr.
171
194
 
172
- ## 🌐 Browser Extension & Web Client
195
+ * **Arguments:**
196
+ * `command` (string, required): The shell command to execute (e.g. `cat .env`, `docker logs web`, `git diff`).
197
+ * `cwd` (string, optional): Working directory.
198
+ * `profile` (string, optional): Detection profile (defaults to `Dev`).
199
+ * `timeout_ms` (number, optional): Timeout in ms (defaults to `15000`).
173
200
 
174
- Looking for real-time protection directly inside your web browser?
175
- * **Chrome Extension:** Get the [PrivacyScrubber Chrome Extension](https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol) to sanitize prompts directly inside ChatGPT, Claude, and Gemini in real-time.
176
- * **Web Sandbox:** Use the zero-server browser sanitization tools at [PrivacyScrubber Homepage](https://privacyscrubber.com/?utm_source=npm&utm_medium=readme&utm_campaign=mcp_server).
201
+ ### 5. `guard_read_file` (Credential-Masking File Reader)
202
+ Reads files (.env, configs, source code, database dumps) and tokenizes all passwords, JWTs, and PII in volatile memory, returning safe redacted content for AI reasoning.
177
203
 
178
- ## πŸ“„ License & Commercial Upgrade
204
+ * **Arguments:**
205
+ * `file_path` (string, required): Path to file.
206
+ * `profile` (string, optional): Detection profile (defaults to `Dev`).
207
+ * `max_lines` (number, optional): Line cap for large files (defaults to `500`).
179
208
 
180
- By default, the server runs under the **Free Tier** (restricted to 15,000 characters per request and the basic `General` PII profile). To unlock 25 specialized engineering, medical, legal, and financial PII profiles, as well as team-wide custom rules, you can purchase a commercial license.
209
+ ### 6. `guard_git_diff` (Pre-Commit & Diff Sanitizer)
210
+ Inspects staged (`--cached`) or unstaged repository diffs, redacting any newly introduced secrets or PII in local RAM before AI code review or commit message generation.
181
211
 
182
- ### Feature Comparison
212
+ * **Arguments:**
213
+ * `staged` (boolean, optional): If `true`, inspects staged changes (`git diff --cached`). Defaults to `false`.
214
+ * `cwd` (string, optional): Working directory.
215
+ * `profile` (string, optional): Detection profile (defaults to `Dev`).
183
216
 
184
- | Feature | Free Tier | PRO Tier | TEAMS Tier |
185
- | :--- | :--- | :--- | :--- |
186
- | **Volatile Tokenization** | βœ… Yes | βœ… Yes | βœ… Yes |
187
- | **Standard PII Masking** | βœ… Yes | βœ… Yes | βœ… Yes |
188
- | **Max Character Length** | 15,000 chars | ♾️ Unlimited | ♾️ Unlimited |
189
- | **Industry Profiles** | General Only | 25 Profiles | 25 Profiles |
190
- | **Custom Regex Rules** | ❌ Locked | ♾️ Unlimited | ♾️ Unlimited |
191
- | **Team Rules Sync (GPO)** | ❌ No | ❌ No | βœ… Yes (Shared Link) |
192
- | **Licensing Cost** | $0 | **$110 Lifetime** | **$99/mo Flat Rate** |
217
+ ### 7. `guard_apply_patch` (Safe Patch Applicator)
218
+ Reverses token placeholders (`[API_KEY_1]`, `[SECRET_1]`) in AI-generated code or text by looking up the local RAM session map, creating a `.bak` backup, and writing authentic cleartext directly to disk. The remote LLM never sees real secrets.
193
219
 
194
- πŸ‘‰ **[Acquire a PRO / TEAMS License Key at privacyscrubber.com/pricing](https://privacyscrubber.com/pricing?utm_source=npm&utm_medium=readme&utm_campaign=mcp_server)**
220
+ * **Arguments:**
221
+ * `file_path` (string, required): Path to target file.
222
+ * `content` (string, required): Content containing tokens to restore on disk.
223
+ * `create_backup` (boolean, optional): Backup existing file before write (defaults to `true`).
195
224
 
196
- ---
225
+ ### 8. `create_agent_rules` (1-Click Agent Rule Injection)
226
+ Automatically scaffolds CISO-grade Zero-Trust directives into `.cursorrules`, `.windsurfrules`, `CLAUDE.md`, `.github/copilot-instructions.md`, or `.clinerules`.
227
+
228
+ * **Arguments:**
229
+ * `agent_types` (array, optional): `["all"]`, `["cursor"]`, `["windsurf"]`, `["claude_code"]`, `["copilot"]`, `["cline"]`. Defaults to `["all"]`.
230
+ * `workspace_dir` (string, optional): Workspace directory.
197
231
 
198
- ### 4. `check_status`
232
+ ### 9. `check_status`
199
233
 
200
234
  Returns a visual dashboard showing your current tier, session request count, active profiles, and upgrade instructions. Use it at any time to check your license status or get setup help.
201
235
 
@@ -210,7 +244,7 @@ Returns a visual dashboard showing your current tier, session request count, act
210
244
  * **Response Example (Free Tier):**
211
245
  ```
212
246
  ╔══════════════════════════════════════════════════╗
213
- β•‘ PrivacyScrubber MCP Server v1.7.1 β•‘
247
+ β•‘ PrivacyScrubber MCP Server v2.2.0 β•‘
214
248
  ╠══════════════════════════════════════════════════╣
215
249
  β•‘ πŸ”“ Tier: FREE β•‘
216
250
  β•‘ πŸ“Š Session requests: 5 β•‘
@@ -231,6 +265,52 @@ Returns a visual dashboard showing your current tier, session request count, act
231
265
 
232
266
  ---
233
267
 
268
+ ## πŸ›‘οΈ Standalone CLI: `ps-guard`
269
+
270
+ PrivacyScrubber bundles `ps-guard` for Unix pipe, pre-commit, and agentic workflows:
271
+
272
+ ```bash
273
+ # Pipe any output through RAM redaction
274
+ cat .env | npx ps-guard --profile dev
275
+
276
+ # Execute commands through the ZTDS safety wrapper
277
+ npx ps-guard -- npm test
278
+
279
+ # Review git diff with secrets redacted
280
+ npx ps-guard --diff --staged
281
+
282
+ # Generate rules for all AI IDEs (.cursorrules, .windsurfrules, CLAUDE.md, copilot, cline)
283
+ npx ps-guard --rules
284
+ ```
285
+
286
+ ---
287
+
288
+ ## 🌐 Browser Extension & Web Client
289
+
290
+ Looking for real-time protection directly inside your web browser?
291
+ * **Chrome Extension:** Get the [PrivacyScrubber Chrome Extension](https://chromewebstore.google.com/detail/privacyscrubber-%E2%80%94-pii-red/pimoejgefeilajmmbpghifdmhdlkgjol) to sanitize prompts directly inside ChatGPT, Claude, and Gemini in real-time.
292
+ * **Web Sandbox:** Use the zero-server browser sanitization tools at [PrivacyScrubber Homepage](https://privacyscrubber.com/?utm_source=npm&utm_medium=readme&utm_campaign=mcp_server).
293
+
294
+ ## πŸ“„ License & Commercial Upgrade
295
+
296
+ By default, the server runs under the **Free Tier** (restricted to 15,000 characters per request and the basic `General` PII profile). To unlock 25 specialized engineering, medical, legal, and financial PII profiles, as well as team-wide custom rules, you can purchase a commercial license.
297
+
298
+ ### Feature Comparison
299
+
300
+ | Feature | Free Tier | PRO Tier | TEAMS Tier |
301
+ | :--- | :--- | :--- | :--- |
302
+ | **Volatile Tokenization** | βœ… Yes | βœ… Yes | βœ… Yes |
303
+ | **Standard PII Masking** | βœ… Yes | βœ… Yes | βœ… Yes |
304
+ | **Max Character Length** | 15,000 chars | ♾️ Unlimited | ♾️ Unlimited |
305
+ | **Industry Profiles** | General Only | 25 Profiles | 25 Profiles |
306
+ | **Custom Regex Rules** | ❌ Locked | ♾️ Unlimited | ♾️ Unlimited |
307
+ | **Team Rules Sync (GPO)** | ❌ No | ❌ No | βœ… Yes (Shared Link) |
308
+ | **Licensing Cost** | $0 | **$110 Lifetime** | **$99/mo Flat Rate** |
309
+
310
+ πŸ‘‰ **[Acquire a PRO / TEAMS License Key at privacyscrubber.com/pricing](https://privacyscrubber.com/pricing?utm_source=npm&utm_medium=readme&utm_campaign=mcp_server)**
311
+
312
+ ---
313
+
234
314
  ## πŸ” After Purchase: Activate PRO in Your MCP Client
235
315
 
236
316
  After purchasing a PRO license at [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing?utm_source=npm&utm_medium=readme&utm_campaign=mcp_server), you will receive a license key. Add it to your MCP client config as an environment variable: `PRIVACYSCRUBBER_KEY`.
package/index.js CHANGED
@@ -24,6 +24,7 @@ import path from 'path';
24
24
  import { fileURLToPath } from 'url';
25
25
 
26
26
  import crypto from 'crypto';
27
+ import { execSync } from 'child_process';
27
28
 
28
29
  const require = createRequire(import.meta.url);
29
30
  const __filename = fileURLToPath(import.meta.url);
@@ -417,6 +418,120 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
417
418
  properties: {},
418
419
  required: []
419
420
  }
421
+ },
422
+ {
423
+ name: "guard_exec",
424
+ description: "Zero-Trust Agentic Guard: Safely executes a shell/terminal command locally in isolated process, sanitizes stdout and stderr in volatile RAM, tokenizes any API keys, credentials, database URIs, or PII before sending output to LLM. Maps original values in memory for safe local restoration.",
425
+ inputSchema: {
426
+ type: "object",
427
+ properties: {
428
+ command: {
429
+ type: "string",
430
+ description: "The shell command to execute (e.g. 'cat .env', 'git diff', 'docker logs web', 'env')."
431
+ },
432
+ cwd: {
433
+ type: "string",
434
+ description: "Optional working directory. Defaults to current workspace root."
435
+ },
436
+ profile: {
437
+ type: "string",
438
+ description: "Detection profile. Available: 'General', 'Dev' (recommended for code/secrets), 'Security', 'Finance', etc. Defaults to 'Dev'."
439
+ },
440
+ timeout_ms: {
441
+ type: "number",
442
+ description: "Execution timeout in milliseconds. Defaults to 15000 (15s)."
443
+ }
444
+ },
445
+ required: ["command"]
446
+ }
447
+ },
448
+ {
449
+ name: "guard_read_file",
450
+ description: "Zero-Trust Agentic Guard: Reads a sensitive local file (.env, config, credentials, source code, database dumps), sanitizes all credentials and PII in volatile RAM, and returns safe tokenized content for AI agent context.",
451
+ inputSchema: {
452
+ type: "object",
453
+ properties: {
454
+ file_path: {
455
+ type: "string",
456
+ description: "Absolute or relative path to the file to inspect."
457
+ },
458
+ profile: {
459
+ type: "string",
460
+ description: "Detection profile. Defaults to 'Dev'."
461
+ },
462
+ max_lines: {
463
+ type: "number",
464
+ description: "Optional line cap for large files. Defaults to 500."
465
+ }
466
+ },
467
+ required: ["file_path"]
468
+ }
469
+ },
470
+ {
471
+ name: "guard_git_diff",
472
+ description: "Zero-Trust Agentic Guard: Inspects unstaged or staged git diffs locally, sanitizes any newly added or modified credentials, tokens, or PII in RAM, and returns safe diff for AI commit generation or code review.",
473
+ inputSchema: {
474
+ type: "object",
475
+ properties: {
476
+ staged: {
477
+ type: "boolean",
478
+ description: "If true, runs 'git diff --cached'. If false, runs 'git diff'. Defaults to false."
479
+ },
480
+ cwd: {
481
+ type: "string",
482
+ description: "Optional repository directory. Defaults to current workspace root."
483
+ },
484
+ profile: {
485
+ type: "string",
486
+ description: "Detection profile. Defaults to 'Dev'."
487
+ }
488
+ },
489
+ required: []
490
+ }
491
+ },
492
+ {
493
+ name: "guard_apply_patch",
494
+ description: "Zero-Trust Agentic Guard: Reverses token placeholders ([API_KEY_1], [SECRET_1]) in AI-generated code or text, restores authentic values from local volatile RAM, and writes real content directly to target file on disk. Remote LLM never learns actual secrets.",
495
+ inputSchema: {
496
+ type: "object",
497
+ properties: {
498
+ file_path: {
499
+ type: "string",
500
+ description: "Absolute path to the destination file to create or update."
501
+ },
502
+ content: {
503
+ type: "string",
504
+ description: "Code or text content containing token placeholders to de-anonymize locally before write."
505
+ },
506
+ create_backup: {
507
+ type: "boolean",
508
+ description: "Whether to create a .bak backup if target file already exists. Defaults to true."
509
+ }
510
+ },
511
+ required: ["file_path", "content"]
512
+ }
513
+ },
514
+ {
515
+ name: "create_agent_rules",
516
+ description: "Zero-Trust Agentic Guard: Scaffolds CISO-grade AI Agent Rules in workspace (.cursorrules, .windsurfrules, CLAUDE.md, .github/copilot-instructions.md, .clinerules). Guides autonomous agents to route all file reads, git diffs, and shell commands through PrivacyScrubber Guard tools.",
517
+ inputSchema: {
518
+ type: "object",
519
+ properties: {
520
+ agent_types: {
521
+ type: "array",
522
+ items: {
523
+ type: "string",
524
+ enum: ["all", "cursor", "windsurf", "claude_code", "copilot", "cline"]
525
+ },
526
+ description: "List of agent targets to generate rules for. Use ['all'] to generate rules for all supported AI agents."
527
+ },
528
+ workspace_dir: {
529
+ type: "string",
530
+ description: "Target workspace root directory. Defaults to current directory."
531
+ }
532
+ },
533
+ required: []
534
+ }
420
535
  }
421
536
  ]
422
537
  };
@@ -433,6 +548,10 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
433
548
  {
434
549
  name: "safe_redaction_workflow",
435
550
  description: "Instructions for using the redact_file tool safely with QA checks.",
551
+ },
552
+ {
553
+ name: "agentic_guard_instructions",
554
+ description: "Zero-Trust Agentic Guard instructions for autonomous AI agents (Cursor, Windsurf, Claude Code).",
436
555
  }
437
556
  ]
438
557
  };
@@ -455,6 +574,21 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
455
574
  };
456
575
  }
457
576
 
577
+ if (request.params.name === "agentic_guard_instructions") {
578
+ return {
579
+ description: "Instructions for autonomous AI agents operating under Zero-Trust Agentic Guard.",
580
+ messages: [
581
+ {
582
+ role: "user",
583
+ content: {
584
+ type: "text",
585
+ text: "You are an autonomous AI software engineer operating under strict Zero-Trust Data Sanitization (ZTDS) policies. Sensitive files, logs, and shell output must never leak API keys, credentials, or PII to external model APIs.\n\nFollow these mandatory agent protocols:\n1. Terminal Commands: When running commands whose output might contain credentials or PII (cat .env, docker logs, env, git diff, psql), use `guard_exec` to sanitize output in local RAM before it enters your context.\n2. Reading Secrets/Config: When reading configuration files, API keys, or database schemas, call `guard_read_file` instead of raw file readers.\n3. Code Review & Commits: Use `guard_git_diff` to review changes safely with credentials masked as tokens.\n4. Writing Code: When writing code containing token placeholders (e.g. [API_KEY_1]), use `guard_apply_patch` to restore authentic values locally on disk without exposing them to the cloud.\n5. AI Answers: Pass final text containing tokens through `reveal_text` before returning it to the user."
586
+ }
587
+ }
588
+ ]
589
+ };
590
+ }
591
+
458
592
  if (request.params.name !== "secure_ai_workflow") {
459
593
  throw new Error(`Unknown prompt: ${request.params.name}`);
460
594
  }
@@ -1288,6 +1422,296 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
1288
1422
  };
1289
1423
  }
1290
1424
 
1425
+ if (name === "guard_exec") {
1426
+ const { command, cwd, profile = "Dev", timeout_ms = 15000 } = args || {};
1427
+ if (!command || typeof command !== "string") {
1428
+ return {
1429
+ isError: true,
1430
+ content: [{ type: "text", text: "Error: Missing required parameter 'command'." }]
1431
+ };
1432
+ }
1433
+ const license = checkLicenseStatus();
1434
+ const limitStatus = checkFreeTierLimit(license.isPro);
1435
+ if (limitStatus.blocked) {
1436
+ return {
1437
+ isError: true,
1438
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted (${FREE_TIER_DAILY_LIMIT} requests). Get a PRO key at https://privacyscrubber.com/pricing` }]
1439
+ };
1440
+ }
1441
+
1442
+ const execCwd = cwd ? path.resolve(cwd) : process.cwd();
1443
+ let stdout = '';
1444
+ let stderr = '';
1445
+ let exitCode = 0;
1446
+
1447
+ try {
1448
+ stdout = execSync(command, {
1449
+ cwd: execCwd,
1450
+ timeout: Math.min(Math.max(timeout_ms, 1000), 60000),
1451
+ maxBuffer: 10 * 1024 * 1024,
1452
+ encoding: 'utf8',
1453
+ stdio: ['pipe', 'pipe', 'pipe']
1454
+ });
1455
+ } catch (err) {
1456
+ stdout = err.stdout ? err.stdout.toString() : '';
1457
+ stderr = err.stderr ? err.stderr.toString() : (err.message || '');
1458
+ exitCode = typeof err.status === 'number' ? err.status : 1;
1459
+ }
1460
+
1461
+ const targetProfile = (profile || "Dev").trim();
1462
+ const isAdvanced = targetProfile.toLowerCase() !== "general";
1463
+ const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
1464
+
1465
+ const { processedText: cleanStdout } = truncateIfFree(stdout, license.isPro, charLimit);
1466
+ const { processedText: cleanStderr } = truncateIfFree(stderr, license.isPro, charLimit);
1467
+
1468
+ const resCommand = performSanitization(command, targetProfile, sessionIgnoreList);
1469
+ const resStdout = performSanitization(cleanStdout, targetProfile, sessionIgnoreList);
1470
+ const resStderr = performSanitization(cleanStderr, targetProfile, sessionIgnoreList);
1471
+
1472
+ const allNewTokens = { ...resCommand.newTokens, ...resStdout.newTokens, ...resStderr.newTokens };
1473
+ const tokenCount = Object.keys(allNewTokens).length;
1474
+ const telemetry = buildCisoAuditTelemetry(allNewTokens);
1475
+ const receiptMd = formatAuditReceipt(telemetry);
1476
+
1477
+ const responseText = `[Zero-Trust Agentic Guard: Exec: ${command.split(' ')[0]}]\n` +
1478
+ `Command: ${resCommand.scrubbedText}\n` +
1479
+ `Exit Code: ${exitCode}\n` +
1480
+ `Tokens Redacted: ${tokenCount}\n\n` +
1481
+ (resStdout.scrubbedText ? `--- STDOUT ---\n${resStdout.scrubbedText}\n\n` : '') +
1482
+ (resStderr.scrubbedText ? `--- STDERR ---\n${resStderr.scrubbedText}\n\n` : '') +
1483
+ receiptMd;
1484
+
1485
+ return {
1486
+ content: [{ type: "text", text: responseText }]
1487
+ };
1488
+ }
1489
+
1490
+ if (name === "guard_read_file") {
1491
+ const { file_path, profile = "Dev", max_lines = 500 } = args || {};
1492
+ if (!file_path || typeof file_path !== "string") {
1493
+ return {
1494
+ isError: true,
1495
+ content: [{ type: "text", text: "Error: Missing required parameter 'file_path'." }]
1496
+ };
1497
+ }
1498
+ const resolvedPath = path.resolve(process.cwd(), file_path);
1499
+ if (!fs.existsSync(resolvedPath)) {
1500
+ return {
1501
+ isError: true,
1502
+ content: [{ type: "text", text: `Error: File not found at path: ${resolvedPath}` }]
1503
+ };
1504
+ }
1505
+ const stat = fs.statSync(resolvedPath);
1506
+ if (stat.isDirectory()) {
1507
+ return {
1508
+ isError: true,
1509
+ content: [{ type: "text", text: `Error: Path is a directory. Use audit_directory_for_pii to scan directories.` }]
1510
+ };
1511
+ }
1512
+ const license = checkLicenseStatus();
1513
+ const limitStatus = checkFreeTierLimit(license.isPro);
1514
+ if (limitStatus.blocked) {
1515
+ return {
1516
+ isError: true,
1517
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted. Get a PRO key at https://privacyscrubber.com/pricing` }]
1518
+ };
1519
+ }
1520
+
1521
+ let rawContent = fs.readFileSync(resolvedPath, 'utf8');
1522
+ const lines = rawContent.split(/\r?\n/);
1523
+ const capped = lines.slice(0, max_lines).join('\n');
1524
+ const wasTruncated = lines.length > max_lines;
1525
+
1526
+ const targetProfile = (profile || "Dev").trim();
1527
+ const isAdvanced = targetProfile.toLowerCase() !== "general";
1528
+ const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
1529
+
1530
+ const { processedText } = truncateIfFree(capped, license.isPro, charLimit);
1531
+ const { scrubbedText, newTokens } = performSanitization(processedText, targetProfile, sessionIgnoreList);
1532
+
1533
+ const telemetry = buildCisoAuditTelemetry(newTokens);
1534
+ const receiptMd = formatAuditReceipt(telemetry);
1535
+
1536
+ const responseText = `[Zero-Trust Agentic Guard: File Read: ${path.basename(resolvedPath)}]\n` +
1537
+ `Path: ${resolvedPath}\n` +
1538
+ `Total Lines: ${lines.length}${wasTruncated ? ` (Displaying first ${max_lines} lines)` : ''}\n` +
1539
+ `Tokens Redacted: ${Object.keys(newTokens).length}\n\n` +
1540
+ `--- SANITIZED CONTENT ---\n` +
1541
+ scrubbedText + '\n\n' +
1542
+ receiptMd;
1543
+
1544
+ return {
1545
+ content: [{ type: "text", text: responseText }]
1546
+ };
1547
+ }
1548
+
1549
+ if (name === "guard_git_diff") {
1550
+ const { staged = false, cwd, profile = "Dev" } = args || {};
1551
+ const repoCwd = cwd ? path.resolve(cwd) : process.cwd();
1552
+ const license = checkLicenseStatus();
1553
+ const limitStatus = checkFreeTierLimit(license.isPro);
1554
+ if (limitStatus.blocked) {
1555
+ return {
1556
+ isError: true,
1557
+ content: [{ type: "text", text: `Error: Free tier daily limit exhausted. Get a PRO key at https://privacyscrubber.com/pricing` }]
1558
+ };
1559
+ }
1560
+
1561
+ const gitCmd = staged ? "git diff --cached" : "git diff";
1562
+ let diffOutput = '';
1563
+ try {
1564
+ diffOutput = execSync(gitCmd, { cwd: repoCwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
1565
+ } catch (err) {
1566
+ if (err.code === 'ENOBUFS' && err.stdout) {
1567
+ diffOutput = err.stdout.toString('utf8');
1568
+ } else {
1569
+ return {
1570
+ isError: true,
1571
+ content: [{ type: "text", text: `Error executing '${gitCmd}': ${err.message}` }]
1572
+ };
1573
+ }
1574
+ }
1575
+
1576
+ if (!diffOutput.trim()) {
1577
+ return {
1578
+ content: [{ type: "text", text: `[Zero-Trust Agentic Guard: Git Diff]\nNo git diff changes detected (${staged ? 'staged' : 'unstaged'}).` }]
1579
+ };
1580
+ }
1581
+
1582
+ const targetProfile = (profile || "Dev").trim();
1583
+ const isAdvanced = targetProfile.toLowerCase() !== "general";
1584
+ const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
1585
+
1586
+ const { processedText } = truncateIfFree(diffOutput, license.isPro, charLimit);
1587
+ const { scrubbedText, newTokens } = performSanitization(processedText, targetProfile, sessionIgnoreList);
1588
+
1589
+ const telemetry = buildCisoAuditTelemetry(newTokens);
1590
+ const receiptMd = formatAuditReceipt(telemetry);
1591
+
1592
+ const responseText = `[Zero-Trust Agentic Guard: Git Diff (${staged ? 'Staged' : 'Unstaged'})]\n` +
1593
+ `Tokens Redacted: ${Object.keys(newTokens).length}\n\n` +
1594
+ `--- SANITIZED DIFF ---\n` +
1595
+ scrubbedText + '\n\n' +
1596
+ receiptMd;
1597
+
1598
+ return {
1599
+ content: [{ type: "text", text: responseText }]
1600
+ };
1601
+ }
1602
+
1603
+ if (name === "guard_apply_patch") {
1604
+ const { file_path, content, create_backup = true } = args || {};
1605
+ if (!file_path || typeof file_path !== "string") {
1606
+ return {
1607
+ isError: true,
1608
+ content: [{ type: "text", text: "Error: Missing required parameter 'file_path'." }]
1609
+ };
1610
+ }
1611
+ if (content === undefined || content === null || typeof content !== "string") {
1612
+ return {
1613
+ isError: true,
1614
+ content: [{ type: "text", text: "Error: Missing required parameter 'content' (must be string)." }]
1615
+ };
1616
+ }
1617
+
1618
+ const resolvedPath = path.resolve(process.cwd(), file_path);
1619
+ const restored = PrivacyScrubberCore.unscrubText(content, sessionMap);
1620
+ const authenticContent = restored.restoredText;
1621
+
1622
+ let backupCreated = false;
1623
+ if (create_backup && fs.existsSync(resolvedPath)) {
1624
+ const backupPath = `${resolvedPath}.bak`;
1625
+ fs.copyFileSync(resolvedPath, backupPath);
1626
+ backupCreated = true;
1627
+ }
1628
+
1629
+ const parentDir = path.dirname(resolvedPath);
1630
+ if (!fs.existsSync(parentDir)) {
1631
+ fs.mkdirSync(parentDir, { recursive: true });
1632
+ }
1633
+
1634
+ fs.writeFileSync(resolvedPath, authenticContent, 'utf8');
1635
+
1636
+ return {
1637
+ content: [{
1638
+ type: "text",
1639
+ text: `[Zero-Trust Agentic Guard: Patch Applied Successfully]\n` +
1640
+ `Destination: ${resolvedPath}\n` +
1641
+ `Tokens Restored Locally: ${restored.restoredCount || 0}\n` +
1642
+ `Backup Created: ${backupCreated ? `${resolvedPath}.bak` : 'No'}\n` +
1643
+ `Status: File written with 100% authentic local values. Remote LLM received zero secrets.`
1644
+ }]
1645
+ };
1646
+ }
1647
+
1648
+ if (name === "create_agent_rules") {
1649
+ const { agent_types = ["all"], workspace_dir } = args || {};
1650
+ const targetDir = workspace_dir ? path.resolve(workspace_dir) : process.cwd();
1651
+ const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline"] : agent_types;
1652
+
1653
+ const ruleText = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
1654
+ # 100% Local In-Memory Data Sanitization. Zero Network Egress.
1655
+
1656
+ ## Agent Mandate
1657
+ You are an autonomous AI assistant operating under strict Zero-Trust Data Sanitization (ZTDS) policies.
1658
+ Before reading sensitive files, running terminal commands that may print credentials, or committing code:
1659
+
1660
+ 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.
1661
+ 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.
1662
+ 3. **Git Code Review:** Use \`guard_git_diff\` to inspect changes without exposing plaintext secrets to the cloud.
1663
+ 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.
1664
+ 5. **Final Output:** Pass any final synthesized text containing token placeholders through \`reveal_text\` before presenting it to the user.
1665
+ `;
1666
+
1667
+ const generatedFiles = [];
1668
+ if (targets.includes("cursor")) {
1669
+ const p = path.join(targetDir, '.cursorrules');
1670
+ fs.writeFileSync(p, ruleText, 'utf8');
1671
+ generatedFiles.push('.cursorrules');
1672
+ }
1673
+ if (targets.includes("windsurf")) {
1674
+ const p = path.join(targetDir, '.windsurfrules');
1675
+ fs.writeFileSync(p, ruleText, 'utf8');
1676
+ generatedFiles.push('.windsurfrules');
1677
+ }
1678
+ if (targets.includes("claude_code")) {
1679
+ const p = path.join(targetDir, 'CLAUDE.md');
1680
+ if (fs.existsSync(p)) {
1681
+ const existing = fs.readFileSync(p, 'utf8');
1682
+ if (!existing.includes("Zero-Trust Agentic Guard")) {
1683
+ fs.writeFileSync(p, existing + "\n\n" + ruleText, 'utf8');
1684
+ generatedFiles.push('CLAUDE.md (appended)');
1685
+ }
1686
+ } else {
1687
+ fs.writeFileSync(p, ruleText, 'utf8');
1688
+ generatedFiles.push('CLAUDE.md');
1689
+ }
1690
+ }
1691
+ if (targets.includes("copilot")) {
1692
+ const copilotDir = path.join(targetDir, '.github');
1693
+ if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
1694
+ const p = path.join(copilotDir, 'copilot-instructions.md');
1695
+ fs.writeFileSync(p, ruleText, 'utf8');
1696
+ generatedFiles.push('.github/copilot-instructions.md');
1697
+ }
1698
+ if (targets.includes("cline")) {
1699
+ const p = path.join(targetDir, '.clinerules');
1700
+ fs.writeFileSync(p, ruleText, 'utf8');
1701
+ generatedFiles.push('.clinerules');
1702
+ }
1703
+
1704
+ return {
1705
+ content: [{
1706
+ type: "text",
1707
+ text: `[Zero-Trust Agentic Guard: Agent Rules Generated]\n` +
1708
+ `Workspace: ${targetDir}\n` +
1709
+ `Files Created/Updated:\n${generatedFiles.map(f => `- ${f}`).join('\n')}\n\n` +
1710
+ `Autonomous AI agents in this repository will now automatically use PrivacyScrubber Guard tools.`
1711
+ }]
1712
+ };
1713
+ }
1714
+
1291
1715
  return {
1292
1716
  isError: true,
1293
1717
  content: [{ type: "text", text: `Unknown tool: ${name}` }]
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "privacyscrubber/pii-masking-mcp",
4
- "version": "2.1.0",
4
+ "version": "2.2.0",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "author": {
7
7
  "name": "PrivacyScrubber",
@@ -12,7 +12,9 @@
12
12
  "entry_point": "index.js",
13
13
  "mcp_config": {
14
14
  "command": "node",
15
- "args": ["${__dirname}/index.js"],
15
+ "args": [
16
+ "${__dirname}/index.js"
17
+ ],
16
18
  "env": {
17
19
  "PRIVACYSCRUBBER_KEY": "${user_config.PRIVACYSCRUBBER_KEY}"
18
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/mcp-server",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "mcpName": "io.github.moxno/privacyscrubber-mcp",
5
5
  "description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
6
6
  "main": "index.js",
@@ -27,6 +27,8 @@
27
27
  "mcp-server": "index.js",
28
28
  "privacyscrubber-mcp": "index.js",
29
29
  "pii-masking-run": "pii-masking-run.js",
30
+ "ps-guard": "pii-masking-run.js",
31
+ "agentic-guard": "pii-masking-run.js",
30
32
  "privacyscrubber-mcp-install": "install.js"
31
33
  },
32
34
  "scripts": {
@@ -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
+ }
package/ps-pii-engine.cjs CHANGED
@@ -92,7 +92,7 @@ let REGEX_RULES = [
92
92
  { type: 'ID', regex: /\b\d{2}\/\d{2}\/\d{4}\b/g },
93
93
 
94
94
  // Phone Numbers
95
- { type: 'PHONE', regex: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
95
+ { type: 'PHONE', regex: /(?<!\w)(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
96
96
  { type: 'PHONE', regex: /\+\d{1,3}[ \t.-]+(?:\(?\d{1,4}\)?[ \t.-]+)?\d{2,8}(?:[ \t.-]+\d{2,8}){0,4}\b/g },
97
97
  { type: 'PHONE', regex: /\+?[1-9]\d{1,3}[\s.-]\(?\d{1,4}\)?[\s.-]\d{2,4}[\s.-]\d{4}/g },
98
98
  { type: 'PHONE', regex: /(?:\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}\b/g },
@@ -119,6 +119,8 @@ let REGEX_RULES = [
119
119
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:(?:[ \t\xA0]*\p{Lu}\.)+[ \t\xA0]*|[ \t\xA0]+)(?:\p{Lu}\p{Ll}[\p{Ll}'-]*|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
120
120
  // Names with Honorifics (with or without period, supporting single or multi-word full names) (Unicode-safe)
121
121
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.|Hon\.)[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*))?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
122
+ // Names with Quoted or Parenthetical Nicknames (e.g. Judith "Judy" Diaz, Robert (Bob) Smith)
123
+ { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:\p{Lu}[\p{Ll}'-]+)[ \t\xA0]+(?:"[\p{Lu}\p{Ll}'-]+"|'[\p{Lu}\p{Ll}'-]+'|[β€œβ€˜][\p{Lu}\p{Ll}'-]+[”’]|\([\p{Lu}\p{Ll}'-]+\))[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]+)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
122
124
 
123
125
  // W-2 Box c Employer Block (Name, Address, and Zip Code)
124
126
  { type: 'NAME', isContextName: true, regex: /(?:(?:\bBox\s+c\b|\bc\.\s*|\bc\s+(?=Employer))\s*(?:Employer(?:'s)?\s*)?(?:name[,\s]+address[,\s]+and\s+ZIP\s+code|name)?|(?:Employer(?:'s)?\s+name[,\s]+address[,\s]+and\s+ZIP\s+code))[\s:#]*([A-Za-z0-9&., \t\xA0'-]{2,45}?)(?=\r?\n|$|\s{3,}|\t|EIN|FEIN|Box|\d+\b|Wages|Federal|Social|Medicare)/gi },
@@ -129,7 +131,11 @@ let REGEX_RULES = [
129
131
  // Contextual Last Names (Last name, Surname, Family name)
130
132
  { type: 'NAME', isContextName: true, regex: /(?:(?:Employee(?:'s)?|Borrower(?:'s)?|Co-Borrower(?:'s)?|Applicant(?:'s)?|Candidate(?:'s)?|Worker(?:'s)?|Taxpayer(?:'s)?|Spouse(?:'s)?|Person(?:'s)?)\s+)?(?:Last\s+name|Surname|Family\s+name)[\s:#]+(?:\b|\b\s*)([A-Za-z'-]{2,30})/gi },
131
133
  // Contextual General Names (Employee, Principal Investigator, Physician, Doctor, etc.)
132
- { type: 'NAME', isContextName: true, regex: /(?:(?:Employee|Employer|Borrower|Co-Borrower|Applicant|Candidate|Worker|Taxpayer|Spouse|Client|Customer|Patient|Claimant|Insured|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Testator|Author)\s+(?:Name|Full\s+Name)|(?:Principal\s+Investigator|Investigator|Physician|Doctor|Supervisor|Reporting\s+To|Prepared\s+By|Attention|Attn|Contact(?:\s+Person)?|Author)(?:\s+Name)?|(?:Employee|Employer|Borrower|Client|Customer|Patient|Claimant|Insured|Doctor|Attn|Signatory|Witness|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Author)\s*[:=#])[:\s#]*(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'-]{1,35}?)(?=\r?\n|$|\s{3,}|\t|Employee|Employer|Address|Phone|SSN|EIN|FEIN|Date|Pay|Rate|Tax|W-2|OMB|Copy|Box|Status)/giu },
134
+ { type: 'NAME', isContextName: true, regex: /(?:(?:Employee|Employer|Borrower|Co-Borrower|Applicant|Candidate|Worker|Taxpayer|Spouse|Client|Customer|Patient|Claimant|Insured|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Testator|Author)\s+(?:Name|Full\s+Name)|(?:Principal\s+Investigator|Investigator|Physician|Doctor|Supervisor|Reporting\s+To|Prepared\s+By|Attention|Attn|(?<!Emergency\s+|ICE\s+)Contact(?:\s+Person)?|Author)(?:\s+Name)?|(?:Employee|Employer|Borrower|Client|Customer|Patient|Claimant|Insured|Doctor|Attn|Signatory|Witness|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Author)\s*[:=#])[:\s#|]*(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'"β€œβ€()-]{1,35}?)(?=\r?\n|$|\s{3,}|\t|Employee|Employer|Address|Phone|SSN|EIN|FEIN|Date|Pay|Rate|Tax|W-2|OMB|Copy|Box|Status|\|)/giu },
135
+ // Contextual Nicknames & Preferred Names (e.g. "Nickname: Judy", "Preferred Name: Alex", "AKA: Bob", "Goes by: Sam")
136
+ { type: 'NAME', isContextName: true, regex: /(?:(?:Patient(?:'s)?|Client(?:'s)?|Child(?:'s)?|Employee(?:'s)?|Person(?:'s)?)\s+)?(?:Nickname|Nick\s+Name|Preferred\s+(?:First\s+)?Name|Also\s+Known\s+As|AKA|Alias|Goes\s+by)[\s:#]+(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'-]{1,30}?)(?=\r?\n|$|\s{3,}|\t|DOB|Date|Phone|Address|Emergency|Relationship|Notes|MRN|\()/giu },
137
+ // Emergency Contacts (Standard, Inverted Last First, Single Name, Table Cells, Extended Relationships, Brackets & Phone Lookahead)
138
+ { type: 'NAME', isContextName: true, regex: /(?:(?:^|[^\p{L}\p{N}_]|\|)\s*(?:In\s+Case\s+of\s+Emergency(?:\s*\(ICE\))?|ICE(?:[ \t\xA0]*(?:Contact|Notification|Person|Name))?|Emergency(?:[ \t\xA0]*(?:Contact|Notification|Person|Info|Details))?|Primary\s+Emergency\s+Contact|Secondary\s+Emergency\s+Contact|Alternate\s+Emergency\s+Contact|Person\s+to\s+notify\s+(?:in\s+case\s+of\s+emergency|in\s+emergency))(?:[ \t\xA0]*(?:[12#]|#\s*\d|\(\d\)))?(?:[ \t\xA0]*(?:Person|Name|Full\s+Name))?(?:[ \t\xA0]*(?:[\/&+-]|and)[ \t\xA0]*(?:Relationship|Relation|Proxy|Guarantor|Guardian))?(?:[ \t\xA0]*\((?:Primary|Secondary|Name|Relationship|Relation|Proxy|Guarantor|Legal\s+Guardian|Full\s+Name)\))?)[:=#|\/\t\s-]*[:=#|\/\t-][ \t\xA0]*([A-Z\p{Lu}][A-Za-z0-9&.'’]*(?:[ \t\xA0]+[A-Za-z0-9&.'’]+)*(?:,[ \t\xA0]*(?!(?:Relationship|Relation|Rel|Mother|Father|Spouse|Wife|Husband|Partner|Son|Daughter|Child|Parent|Brother|Sister|Sibling|Friend|Aunt|Uncle|Cousin|Guardian|Caregiver|Carer|Neighbor|Relative|Family|Grandmother|Grandfather|Grandparent|Grandson|Granddaughter|Stepmother|Stepfather|Stepson|Stepdaughter|Step-mother|Step-father|In-law|Mother-in-law|Father-in-law|Sister-in-law|Brother-in-law|Son-in-law|Daughter-in-law|FiancΓ©|Fiance|Fiancee|Significant\s+Other|Domestic\s+Partner|Life\s+Partner|Roommate|Colleague|Coworker|Co-worker|Legal\s+Guardian|Case\s+Worker|Caseworker|Social\s+Worker|Foster\s+Parent|POA|Power\s+of\s+Attorney|Healthcare\s+Proxy|Medical\s+Proxy|Proxy|Next\s+of\s+Kin|Aide)\b)[A-Z\p{Lu}][A-Za-z0-9&.'’]*(?:[ \t\xA0]+[A-Za-z0-9&.'’]+)*)?)(?=[ \t\xA0]*(?:\([A-Za-z0-9\s\/&':.+–-]+\)|\[[A-Za-z0-9\s\/&':.+–-]+\]|\{[A-Za-z0-9\s\/&':.+–-]+\}|[,\/|\-–—]{1,3}\s*(?:Relationship|Relation|Rel|Mother|Father|Spouse|Wife|Husband|Partner|Son|Daughter|Child|Parent|Brother|Sister|Sibling|Friend|Aunt|Uncle|Cousin|Guardian|Caregiver|Carer|Neighbor|Relative|Family|Grandmother|Grandfather|Grandparent|Grandson|Granddaughter|Stepmother|Stepfather|Stepson|Stepdaughter|Step-mother|Step-father|In-law|Mother-in-law|Father-in-law|Sister-in-law|Brother-in-law|Son-in-law|Daughter-in-law|FiancΓ©|Fiance|Fiancee|Significant\s+Other|Domestic\s+Partner|Life\s+Partner|Roommate|Colleague|Coworker|Co-worker|Legal\s+Guardian|Case\s+Worker|Caseworker|Social\s+Worker|Foster\s+Parent|POA|Power\s+of\s+Attorney|Healthcare\s+Proxy|Medical\s+Proxy|Proxy|Next\s+of\s+Kin|Aide)\b|[,\/|\-–—]?[ \t\xA0]*(?:Phone|Tel|Cell|Mobile|Work|Home|Office|Direct|Telephone)[:\s#-]*|[,\/|\-–—]?[ \t\xA0]*(?:Relationship|Relation|Rel)[:\s#-]*|[,\/|\-–—]?[ \t\xA0]*(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b|[,\/|\-–—]?[ \t\xA0]*\d{3}[-.\s]?\d{4}\b|[ \t\xA0]*(?:\||\r?\n|$|\s{2,}|\t)))/gui },
133
139
  // Box e shorthand
134
140
  { type: 'NAME', isContextName: true, regex: /\b(?:Box\s+e)\s*[:#-]\s*([A-Za-z0-9&.,\s'-]{2,40})/gi }
135
141
  ];
@@ -181,7 +187,9 @@ let PROFILE_RULES = {
181
187
  { type: 'ID', regex: /\b(?:FEIN|EIN|TIN|Tax\s*ID)[:\s#]*(\d{2}-\d{7})\b/gi },
182
188
  { type: 'ID', regex: /\b(?:CRD|CIK|Advisor\s*ID|Broker\s*ID)[:\s#]*(\d{5,10})\b/gi },
183
189
  { type: 'ID', regex: /\b(?:LEI|LEGAL[\s-]ENTITY[\s-]IDENTIFIER)[:\s#-]*[0-9A-Z]{20}\b/gi },
184
- { type: 'ID', regex: /\bBBG[0-9A-Z]{9}\b/g }
190
+ { type: 'ID', regex: /\bBBG[0-9A-Z]{9}\b/g },
191
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Wire(?:\s+(?:Transfer|Amount))?|Transfer|Balance|Deposit|Withdrawal|Settlement|Principal|Interest|Remittance|Payout|Transaction(?:\s+Amount)?|Payment(?:\s+Amount)?|Total|Amount)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£Β₯β‚ͺβ‚½β‚Ή]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£Β₯β‚ͺβ‚½β‚Ή])|[0-9,.'’]{3,}\b)/gi },
192
+ { type: 'FINANCIAL', regex: /(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£Β₯β‚ͺβ‚½β‚Ή]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£Β₯β‚ͺβ‚½β‚Ή]))/gi }
185
193
  ],
186
194
  medical: [
187
195
  { type: 'PHI', regex: /\b(?:MRN|Patient ID|Medical Record (?:No\.?|Num(?:ber)?|#)|Patient (?:No\.?|Num(?:ber)?|#))[\s:#]+([A-Za-z0-9-]+)/gi },
@@ -409,7 +417,7 @@ let PROFILE_RULES = {
409
417
  { type: 'FINANCIAL', regex: /\b(?:Loss|Claim\s+Amount|Claim\s+Settlement|Settlement|Reserve|Indemnity)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
410
418
  { type: 'ID', regex: /\b(?:(?:Assigned\s+)?Adjuster\s+(?:ID|No)|Claim\s+Rep(?:\.|resentative)?)[:\s#]*[-A-Z0-9]{4,15}\b/gi },
411
419
  { type: 'ID', regex: /\bNAIC(?:\s+(?:Company\s+)?(?:Code|CoCode|No\.?|Number|#))?[:\s#]*(\d{5})\b/gi },
412
- { type: 'FINANCIAL', regex: /\b(?:Deductible|Premium|Coverage\s+Amount)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'']+\b/gi },
420
+ { type: 'FINANCIAL', regex: /\b(?:Deductible|Premium|Coverage\s+Amount)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
413
421
  { type: 'ID', regex: /\b[A-HJ-NPR-Z0-9]{17}\b/g },
414
422
  { type: 'ID', regex: /\b(?:Insured|Named\s+Insured)[:\s]+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+/g },
415
423
  { type: 'ID', regex: /\b(?:Agent|Producer)\s*(?:Code|No|ID)[:\s#]*[-A-Z0-9]{4,15}\b/gi },
@@ -421,11 +429,11 @@ let PROFILE_RULES = {
421
429
  accounting: [
422
430
  { type: 'ID', regex: /\b(?:EIN|FEIN|Tax\s+ID)[:\s#]*\d{2}-\d{7}\b/gi },
423
431
  { type: 'FINANCIAL', regex: /\b(?:AGI|Adjusted\s+Gross\s+Income|Taxable\s+Income|Total\s+Income)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
424
- { type: 'FINANCIAL', regex: /\b(?:Form\s+|Withheld\s+)?Box\s+\d{1,2}[a-z]?(?:\s*:\s*[A-Z])?\s*[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
432
+ { type: 'FINANCIAL', regex: /\b(?:Form\s+|Withheld\s+)?Box\s+\d{1,2}[a-z]?(?:\s*:\s*[A-Z])?\s*[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
425
433
  { type: 'ID', regex: /\b(?:Form|Schedule)\s+(?:1040|1040-SR|W-2|W-4|1099-[A-Z]{1,4}|K-1|941|990|4562)\b/gi },
426
- { type: 'FINANCIAL', regex: /\b(?:Federal|State|Assessed)?\s*(?:Tax\s+Due|Balance\s+Due|Overpayment|Refund)[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
434
+ { type: 'FINANCIAL', regex: /\b(?:Federal|State|Assessed)?\s*(?:Tax\s+Due|Balance\s+Due|Overpayment|Refund)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
427
435
  { type: 'ID', regex: /\b(?:State\s+Tax\s+ID|SUI|UI\s+Account\s+No)[:\s#]*[A-Z]{0,3}[-]?\d{4,15}\b/gi },
428
- { type: 'FINANCIAL', regex: /\b(?:Gross|Net|Medicare|Social\s+Security)\s+(?:Pay|Wages|Tips|Withholding)[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
436
+ { type: 'FINANCIAL', regex: /\b(?:Gross|Net|Medicare|Social\s+Security)\s+(?:Pay|Wages|Tips|Withholding)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
429
437
  { type: 'ID', regex: /\b(?:CAF(?:\s+ID)?|(?:Practitioner|Tax\s+Preparer)\s+(?:PIN|ID)|PTIN)[:\s#]*[A-Z0-9-]{6,12}\b/gi },
430
438
  { type: 'ID', regex: /\bEFIN[:\s#]*\d{6}\b/gi },
431
439
  { type: 'ID', regex: /\bGL[-_\s]*(?:Account|Acct|Code)?[:\s#]*\d{4,10}\b/gi },
@@ -829,7 +837,7 @@ const PROFILE_JARGON = {
829
837
  'underwriting': 'underwriting', 'lending': 'underwriting', 'mortgage': 'underwriting', 'loan': 'underwriting', 'income': 'underwriting', 'income_verification': 'underwriting', 'payroll': 'underwriting', 'w2': 'underwriting', 'paystub': 'underwriting',
830
838
  'medical': 'medical', 'healthcare': 'medical', 'health': 'medical', 'pharma': 'pharma', 'hipaa': 'medical',
831
839
  'engineering': 'engineering', 'dev': 'dev', 'api': 'dev', 'devops': 'engineering', 'tech': 'tech',
832
- 'finance': 'finance', 'bizops': 'bizops', 'sales': 'sales', 'wealthmgmt': 'wealthmgmt', 'wealth': 'wealthmgmt', 'insurance': 'insurance', 'accounting': 'accounting', 'pci': 'finance',
840
+ 'finance': 'finance', 'financial': 'finance', 'bizops': 'bizops', 'sales': 'sales', 'wealthmgmt': 'wealthmgmt', 'wealth': 'wealthmgmt', 'insurance': 'insurance', 'accounting': 'accounting', 'pci': 'finance',
833
841
  'legal': 'legal', 'compliance': 'compliance', 'ccpa': 'ccpa', 'gdpr': 'compliance', 'soc2': 'compliance', 'iso27001': 'compliance', 'nist': 'compliance', 'dpo': 'compliance', 'grc': 'compliance',
834
842
  'hr': 'hr', 'security': 'security', 'marketing': 'marketing', 'support': 'support',
835
843
  'realestate': 'realestate', 'academic': 'academic', 'agents': 'agents', 'ai_agents': 'agents', 'creative': 'creative', 'personal': 'personal'
@@ -847,16 +855,18 @@ const PROFILE_JARGON = {
847
855
  }
848
856
 
849
857
  function stitchOrphanedNameLines(text, profile) {
850
- if (profile === 'medical') {
851
- text = text.replace(/Patient Name:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Patient Name: $1');
852
- } else if (profile === 'legal') {
853
- text = text.replace(/Defendant:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
858
+ if (!text || !text.includes('\n')) return text;
859
+ const prof = (profile || 'general').toLowerCase();
860
+ if (prof === 'medical' || prof === 'general') {
861
+ text = text.replace(/(Patient\s+Name|Emergency\s+Contact|In\s+Case\s+of\s+Emergency|ICE(?:\s+Contact)?):\s*\r?\n+([A-Z\p{Lu}][A-Za-z0-9&.,'’ \t-]*?[A-Za-z0-9\p{Lu}\p{Ll}])(?=\r?\n|$)/gui, '$1: $2');
862
+ } else if (prof === 'legal') {
863
+ text = text.replace(/Defendant:\s*\r?\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
854
864
  }
855
865
  return text;
856
866
  }
857
867
 
858
868
  function detectMatches(text, activeProfile = 'general', customRules = [], nlpNames = [], enabledEntities = null, mlEntities = []) {
859
- let textToProcess = text.replace(/[\u200b\u200c\u200d\ufeff]/g, '');
869
+ let textToProcess = stitchOrphanedNameLines(text, activeProfile).replace(/[\u200b\u200c\u200d\ufeff]/g, '');
860
870
  textToProcess = textToProcess.replace(/([a-z])(Email|Phone|Mobile|Tel|Address|IP|ID|URL|SSN|Date):/g, '$1 $2:');
861
871
 
862
872
  let matches = [];
package/ps-pii-engine.js CHANGED
@@ -92,7 +92,7 @@ let REGEX_RULES = [
92
92
  { type: 'ID', regex: /\b\d{2}\/\d{2}\/\d{4}\b/g },
93
93
 
94
94
  // Phone Numbers
95
- { type: 'PHONE', regex: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
95
+ { type: 'PHONE', regex: /(?<!\w)(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g },
96
96
  { type: 'PHONE', regex: /\+\d{1,3}[ \t.-]+(?:\(?\d{1,4}\)?[ \t.-]+)?\d{2,8}(?:[ \t.-]+\d{2,8}){0,4}\b/g },
97
97
  { type: 'PHONE', regex: /\+?[1-9]\d{1,3}[\s.-]\(?\d{1,4}\)?[\s.-]\d{2,4}[\s.-]\d{4}/g },
98
98
  { type: 'PHONE', regex: /(?:\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}\b/g },
@@ -119,6 +119,8 @@ let REGEX_RULES = [
119
119
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])\p{Lu}{2,}(?:[\p{Lu}'-]*\p{Lu})?(?:(?:[ \t\xA0]*\p{Lu}\.)+[ \t\xA0]*|[ \t\xA0]+)(?:\p{Lu}\p{Ll}[\p{Ll}'-]*|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
120
120
  // Names with Honorifics (with or without period, supporting single or multi-word full names) (Unicode-safe)
121
121
  { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:Mr|Mrs|Ms|Dr|Prof|Hon|Mr\.|Mrs\.|Ms\.|Dr\.|Prof\.|Hon\.)[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*)(?:[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]*\p{Ll}|\p{Lu}[\p{Ll}'-]*[\p{Lu}'-][\p{Ll}'-]*))?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
122
+ // Names with Quoted or Parenthetical Nicknames (e.g. Judith "Judy" Diaz, Robert (Bob) Smith)
123
+ { type: 'NAME', regex: /(?<=^|[^\p{L}\p{N}_])(?:\p{Lu}[\p{Ll}'-]+)[ \t\xA0]+(?:"[\p{Lu}\p{Ll}'-]+"|'[\p{Lu}\p{Ll}'-]+'|[β€œβ€˜][\p{Lu}\p{Ll}'-]+[”’]|\([\p{Lu}\p{Ll}'-]+\))[ \t\xA0]+(?:\p{Lu}[\p{Ll}'-]+)(?:'s)?(?=[^\p{L}\p{N}_]|$)(?![ \t\xA0]*:)/gu },
122
124
 
123
125
  // W-2 Box c Employer Block (Name, Address, and Zip Code)
124
126
  { type: 'NAME', isContextName: true, regex: /(?:(?:\bBox\s+c\b|\bc\.\s*|\bc\s+(?=Employer))\s*(?:Employer(?:'s)?\s*)?(?:name[,\s]+address[,\s]+and\s+ZIP\s+code|name)?|(?:Employer(?:'s)?\s+name[,\s]+address[,\s]+and\s+ZIP\s+code))[\s:#]*([A-Za-z0-9&., \t\xA0'-]{2,45}?)(?=\r?\n|$|\s{3,}|\t|EIN|FEIN|Box|\d+\b|Wages|Federal|Social|Medicare)/gi },
@@ -129,7 +131,11 @@ let REGEX_RULES = [
129
131
  // Contextual Last Names (Last name, Surname, Family name)
130
132
  { type: 'NAME', isContextName: true, regex: /(?:(?:Employee(?:'s)?|Borrower(?:'s)?|Co-Borrower(?:'s)?|Applicant(?:'s)?|Candidate(?:'s)?|Worker(?:'s)?|Taxpayer(?:'s)?|Spouse(?:'s)?|Person(?:'s)?)\s+)?(?:Last\s+name|Surname|Family\s+name)[\s:#]+(?:\b|\b\s*)([A-Za-z'-]{2,30})/gi },
131
133
  // Contextual General Names (Employee, Principal Investigator, Physician, Doctor, etc.)
132
- { type: 'NAME', isContextName: true, regex: /(?:(?:Employee|Employer|Borrower|Co-Borrower|Applicant|Candidate|Worker|Taxpayer|Spouse|Client|Customer|Patient|Claimant|Insured|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Testator|Author)\s+(?:Name|Full\s+Name)|(?:Principal\s+Investigator|Investigator|Physician|Doctor|Supervisor|Reporting\s+To|Prepared\s+By|Attention|Attn|Contact(?:\s+Person)?|Author)(?:\s+Name)?|(?:Employee|Employer|Borrower|Client|Customer|Patient|Claimant|Insured|Doctor|Attn|Signatory|Witness|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Author)\s*[:=#])[:\s#]*(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'-]{1,35}?)(?=\r?\n|$|\s{3,}|\t|Employee|Employer|Address|Phone|SSN|EIN|FEIN|Date|Pay|Rate|Tax|W-2|OMB|Copy|Box|Status)/giu },
134
+ { type: 'NAME', isContextName: true, regex: /(?:(?:Employee|Employer|Borrower|Co-Borrower|Applicant|Candidate|Worker|Taxpayer|Spouse|Client|Customer|Patient|Claimant|Insured|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Testator|Author)\s+(?:Name|Full\s+Name)|(?:Principal\s+Investigator|Investigator|Physician|Doctor|Supervisor|Reporting\s+To|Prepared\s+By|Attention|Attn|(?<!Emergency\s+|ICE\s+)Contact(?:\s+Person)?|Author)(?:\s+Name)?|(?:Employee|Employer|Borrower|Client|Customer|Patient|Claimant|Insured|Doctor|Attn|Signatory|Witness|Tenant|Landlord|Buyer|Seller|Plaintiff|Defendant|Author)\s*[:=#])[:\s#|]*(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'"β€œβ€()-]{1,35}?)(?=\r?\n|$|\s{3,}|\t|Employee|Employer|Address|Phone|SSN|EIN|FEIN|Date|Pay|Rate|Tax|W-2|OMB|Copy|Box|Status|\|)/giu },
135
+ // Contextual Nicknames & Preferred Names (e.g. "Nickname: Judy", "Preferred Name: Alex", "AKA: Bob", "Goes by: Sam")
136
+ { type: 'NAME', isContextName: true, regex: /(?:(?:Patient(?:'s)?|Client(?:'s)?|Child(?:'s)?|Employee(?:'s)?|Person(?:'s)?)\s+)?(?:Nickname|Nick\s+Name|Preferred\s+(?:First\s+)?Name|Also\s+Known\s+As|AKA|Alias|Goes\s+by)[\s:#]+(?:\b|\b\s*)([A-Z\p{Lu}][A-Za-z0-9&.,\s'-]{1,30}?)(?=\r?\n|$|\s{3,}|\t|DOB|Date|Phone|Address|Emergency|Relationship|Notes|MRN|\()/giu },
137
+ // Emergency Contacts (Standard, Inverted Last First, Single Name, Table Cells, Extended Relationships, Brackets & Phone Lookahead)
138
+ { type: 'NAME', isContextName: true, regex: /(?:(?:^|[^\p{L}\p{N}_]|\|)\s*(?:In\s+Case\s+of\s+Emergency(?:\s*\(ICE\))?|ICE(?:[ \t\xA0]*(?:Contact|Notification|Person|Name))?|Emergency(?:[ \t\xA0]*(?:Contact|Notification|Person|Info|Details))?|Primary\s+Emergency\s+Contact|Secondary\s+Emergency\s+Contact|Alternate\s+Emergency\s+Contact|Person\s+to\s+notify\s+(?:in\s+case\s+of\s+emergency|in\s+emergency))(?:[ \t\xA0]*(?:[12#]|#\s*\d|\(\d\)))?(?:[ \t\xA0]*(?:Person|Name|Full\s+Name))?(?:[ \t\xA0]*(?:[\/&+-]|and)[ \t\xA0]*(?:Relationship|Relation|Proxy|Guarantor|Guardian))?(?:[ \t\xA0]*\((?:Primary|Secondary|Name|Relationship|Relation|Proxy|Guarantor|Legal\s+Guardian|Full\s+Name)\))?)[:=#|\/\t\s-]*[:=#|\/\t-][ \t\xA0]*([A-Z\p{Lu}][A-Za-z0-9&.'’]*(?:[ \t\xA0]+[A-Za-z0-9&.'’]+)*(?:,[ \t\xA0]*(?!(?:Relationship|Relation|Rel|Mother|Father|Spouse|Wife|Husband|Partner|Son|Daughter|Child|Parent|Brother|Sister|Sibling|Friend|Aunt|Uncle|Cousin|Guardian|Caregiver|Carer|Neighbor|Relative|Family|Grandmother|Grandfather|Grandparent|Grandson|Granddaughter|Stepmother|Stepfather|Stepson|Stepdaughter|Step-mother|Step-father|In-law|Mother-in-law|Father-in-law|Sister-in-law|Brother-in-law|Son-in-law|Daughter-in-law|FiancΓ©|Fiance|Fiancee|Significant\s+Other|Domestic\s+Partner|Life\s+Partner|Roommate|Colleague|Coworker|Co-worker|Legal\s+Guardian|Case\s+Worker|Caseworker|Social\s+Worker|Foster\s+Parent|POA|Power\s+of\s+Attorney|Healthcare\s+Proxy|Medical\s+Proxy|Proxy|Next\s+of\s+Kin|Aide)\b)[A-Z\p{Lu}][A-Za-z0-9&.'’]*(?:[ \t\xA0]+[A-Za-z0-9&.'’]+)*)?)(?=[ \t\xA0]*(?:\([A-Za-z0-9\s\/&':.+–-]+\)|\[[A-Za-z0-9\s\/&':.+–-]+\]|\{[A-Za-z0-9\s\/&':.+–-]+\}|[,\/|\-–—]{1,3}\s*(?:Relationship|Relation|Rel|Mother|Father|Spouse|Wife|Husband|Partner|Son|Daughter|Child|Parent|Brother|Sister|Sibling|Friend|Aunt|Uncle|Cousin|Guardian|Caregiver|Carer|Neighbor|Relative|Family|Grandmother|Grandfather|Grandparent|Grandson|Granddaughter|Stepmother|Stepfather|Stepson|Stepdaughter|Step-mother|Step-father|In-law|Mother-in-law|Father-in-law|Sister-in-law|Brother-in-law|Son-in-law|Daughter-in-law|FiancΓ©|Fiance|Fiancee|Significant\s+Other|Domestic\s+Partner|Life\s+Partner|Roommate|Colleague|Coworker|Co-worker|Legal\s+Guardian|Case\s+Worker|Caseworker|Social\s+Worker|Foster\s+Parent|POA|Power\s+of\s+Attorney|Healthcare\s+Proxy|Medical\s+Proxy|Proxy|Next\s+of\s+Kin|Aide)\b|[,\/|\-–—]?[ \t\xA0]*(?:Phone|Tel|Cell|Mobile|Work|Home|Office|Direct|Telephone)[:\s#-]*|[,\/|\-–—]?[ \t\xA0]*(?:Relationship|Relation|Rel)[:\s#-]*|[,\/|\-–—]?[ \t\xA0]*(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b|[,\/|\-–—]?[ \t\xA0]*\d{3}[-.\s]?\d{4}\b|[ \t\xA0]*(?:\||\r?\n|$|\s{2,}|\t)))/gui },
133
139
  // Box e shorthand
134
140
  { type: 'NAME', isContextName: true, regex: /\b(?:Box\s+e)\s*[:#-]\s*([A-Za-z0-9&.,\s'-]{2,40})/gi }
135
141
  ];
@@ -181,7 +187,9 @@ let PROFILE_RULES = {
181
187
  { type: 'ID', regex: /\b(?:FEIN|EIN|TIN|Tax\s*ID)[:\s#]*(\d{2}-\d{7})\b/gi },
182
188
  { type: 'ID', regex: /\b(?:CRD|CIK|Advisor\s*ID|Broker\s*ID)[:\s#]*(\d{5,10})\b/gi },
183
189
  { type: 'ID', regex: /\b(?:LEI|LEGAL[\s-]ENTITY[\s-]IDENTIFIER)[:\s#-]*[0-9A-Z]{20}\b/gi },
184
- { type: 'ID', regex: /\bBBG[0-9A-Z]{9}\b/g }
190
+ { type: 'ID', regex: /\bBBG[0-9A-Z]{9}\b/g },
191
+ { type: 'FINANCIAL', regex: /(?<=\b(?:Wire(?:\s+(?:Transfer|Amount))?|Transfer|Balance|Deposit|Withdrawal|Settlement|Principal|Interest|Remittance|Payout|Transaction(?:\s+Amount)?|Payment(?:\s+Amount)?|Total|Amount)[:\s]+)(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£Β₯β‚ͺβ‚½β‚Ή]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£Β₯β‚ͺβ‚½β‚Ή])|[0-9,.'’]{3,}\b)/gi },
192
+ { type: 'FINANCIAL', regex: /(?:(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\s*|[$€£Β₯β‚ͺβ‚½β‚Ή]\s*)[0-9,.'’]*\d[KMB]?\b|\b\d[0-9,.'’]*\d?[KMB]?\s*(?:\b(?:USD|EUR|GBP|CHF|ILS|RUB)\b|[$€£Β₯β‚ͺβ‚½β‚Ή]))/gi }
185
193
  ],
186
194
  medical: [
187
195
  { type: 'PHI', regex: /\b(?:MRN|Patient ID|Medical Record (?:No\.?|Num(?:ber)?|#)|Patient (?:No\.?|Num(?:ber)?|#))[\s:#]+([A-Za-z0-9-]+)/gi },
@@ -409,7 +417,7 @@ let PROFILE_RULES = {
409
417
  { type: 'FINANCIAL', regex: /\b(?:Loss|Claim\s+Amount|Claim\s+Settlement|Settlement|Reserve|Indemnity)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
410
418
  { type: 'ID', regex: /\b(?:(?:Assigned\s+)?Adjuster\s+(?:ID|No)|Claim\s+Rep(?:\.|resentative)?)[:\s#]*[-A-Z0-9]{4,15}\b/gi },
411
419
  { type: 'ID', regex: /\bNAIC(?:\s+(?:Company\s+)?(?:Code|CoCode|No\.?|Number|#))?[:\s#]*(\d{5})\b/gi },
412
- { type: 'FINANCIAL', regex: /\b(?:Deductible|Premium|Coverage\s+Amount)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'']+\b/gi },
420
+ { type: 'FINANCIAL', regex: /\b(?:Deductible|Premium|Coverage\s+Amount)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
413
421
  { type: 'ID', regex: /\b[A-HJ-NPR-Z0-9]{17}\b/g },
414
422
  { type: 'ID', regex: /\b(?:Insured|Named\s+Insured)[:\s]+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+/g },
415
423
  { type: 'ID', regex: /\b(?:Agent|Producer)\s*(?:Code|No|ID)[:\s#]*[-A-Z0-9]{4,15}\b/gi },
@@ -421,11 +429,11 @@ let PROFILE_RULES = {
421
429
  accounting: [
422
430
  { type: 'ID', regex: /\b(?:EIN|FEIN|Tax\s+ID)[:\s#]*\d{2}-\d{7}\b/gi },
423
431
  { type: 'FINANCIAL', regex: /\b(?:AGI|Adjusted\s+Gross\s+Income|Taxable\s+Income|Total\s+Income)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’]+\b/gi },
424
- { type: 'FINANCIAL', regex: /\b(?:Form\s+|Withheld\s+)?Box\s+\d{1,2}[a-z]?(?:\s*:\s*[A-Z])?\s*[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
432
+ { type: 'FINANCIAL', regex: /\b(?:Form\s+|Withheld\s+)?Box\s+\d{1,2}[a-z]?(?:\s*:\s*[A-Z])?\s*[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
425
433
  { type: 'ID', regex: /\b(?:Form|Schedule)\s+(?:1040|1040-SR|W-2|W-4|1099-[A-Z]{1,4}|K-1|941|990|4562)\b/gi },
426
- { type: 'FINANCIAL', regex: /\b(?:Federal|State|Assessed)?\s*(?:Tax\s+Due|Balance\s+Due|Overpayment|Refund)[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
434
+ { type: 'FINANCIAL', regex: /\b(?:Federal|State|Assessed)?\s*(?:Tax\s+Due|Balance\s+Due|Overpayment|Refund)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
427
435
  { type: 'ID', regex: /\b(?:State\s+Tax\s+ID|SUI|UI\s+Account\s+No)[:\s#]*[A-Z]{0,3}[-]?\d{4,15}\b/gi },
428
- { type: 'FINANCIAL', regex: /\b(?:Gross|Net|Medicare|Social\s+Security)\s+(?:Pay|Wages|Tips|Withholding)[:\s]+(?:[$\u20ac\u00a3\u00a5\u20aa\u20bd\u20b9]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.\u2018\u2019]+\b/gi },
436
+ { type: 'FINANCIAL', regex: /\b(?:Gross|Net|Medicare|Social\s+Security)\s+(?:Pay|Wages|Tips|Withholding)[:\s]+(?:[$€£Β₯β‚ͺβ‚½β‚Ή]|(?:USD|EUR|GBP|CHF|ILS|RUB)\s?)[\d,.'’\u2018\u2019]+\b/gi },
429
437
  { type: 'ID', regex: /\b(?:CAF(?:\s+ID)?|(?:Practitioner|Tax\s+Preparer)\s+(?:PIN|ID)|PTIN)[:\s#]*[A-Z0-9-]{6,12}\b/gi },
430
438
  { type: 'ID', regex: /\bEFIN[:\s#]*\d{6}\b/gi },
431
439
  { type: 'ID', regex: /\bGL[-_\s]*(?:Account|Acct|Code)?[:\s#]*\d{4,10}\b/gi },
@@ -829,7 +837,7 @@ const PROFILE_JARGON = {
829
837
  'underwriting': 'underwriting', 'lending': 'underwriting', 'mortgage': 'underwriting', 'loan': 'underwriting', 'income': 'underwriting', 'income_verification': 'underwriting', 'payroll': 'underwriting', 'w2': 'underwriting', 'paystub': 'underwriting',
830
838
  'medical': 'medical', 'healthcare': 'medical', 'health': 'medical', 'pharma': 'pharma', 'hipaa': 'medical',
831
839
  'engineering': 'engineering', 'dev': 'dev', 'api': 'dev', 'devops': 'engineering', 'tech': 'tech',
832
- 'finance': 'finance', 'bizops': 'bizops', 'sales': 'sales', 'wealthmgmt': 'wealthmgmt', 'wealth': 'wealthmgmt', 'insurance': 'insurance', 'accounting': 'accounting', 'pci': 'finance',
840
+ 'finance': 'finance', 'financial': 'finance', 'bizops': 'bizops', 'sales': 'sales', 'wealthmgmt': 'wealthmgmt', 'wealth': 'wealthmgmt', 'insurance': 'insurance', 'accounting': 'accounting', 'pci': 'finance',
833
841
  'legal': 'legal', 'compliance': 'compliance', 'ccpa': 'ccpa', 'gdpr': 'compliance', 'soc2': 'compliance', 'iso27001': 'compliance', 'nist': 'compliance', 'dpo': 'compliance', 'grc': 'compliance',
834
842
  'hr': 'hr', 'security': 'security', 'marketing': 'marketing', 'support': 'support',
835
843
  'realestate': 'realestate', 'academic': 'academic', 'agents': 'agents', 'ai_agents': 'agents', 'creative': 'creative', 'personal': 'personal'
@@ -847,16 +855,18 @@ const PROFILE_JARGON = {
847
855
  }
848
856
 
849
857
  function stitchOrphanedNameLines(text, profile) {
850
- if (profile === 'medical') {
851
- text = text.replace(/Patient Name:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Patient Name: $1');
852
- } else if (profile === 'legal') {
853
- text = text.replace(/Defendant:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
858
+ if (!text || !text.includes('\n')) return text;
859
+ const prof = (profile || 'general').toLowerCase();
860
+ if (prof === 'medical' || prof === 'general') {
861
+ text = text.replace(/(Patient\s+Name|Emergency\s+Contact|In\s+Case\s+of\s+Emergency|ICE(?:\s+Contact)?):\s*\r?\n+([A-Z\p{Lu}][A-Za-z0-9&.,'’ \t-]*?[A-Za-z0-9\p{Lu}\p{Ll}])(?=\r?\n|$)/gui, '$1: $2');
862
+ } else if (prof === 'legal') {
863
+ text = text.replace(/Defendant:\s*\r?\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
854
864
  }
855
865
  return text;
856
866
  }
857
867
 
858
868
  function detectMatches(text, activeProfile = 'general', customRules = [], nlpNames = [], enabledEntities = null, mlEntities = []) {
859
- let textToProcess = text.replace(/[\u200b\u200c\u200d\ufeff]/g, '');
869
+ let textToProcess = stitchOrphanedNameLines(text, activeProfile).replace(/[\u200b\u200c\u200d\ufeff]/g, '');
860
870
  textToProcess = textToProcess.replace(/([a-z])(Email|Phone|Mobile|Tel|Address|IP|ID|URL|SSN|Date):/g, '$1 $2:');
861
871
 
862
872
  let matches = [];
package/scrubber-core.cjs CHANGED
@@ -38,10 +38,10 @@
38
38
  function stitchOrphanedNameLines(text, profile) {
39
39
  if (!text || !text.includes('\n')) return text;
40
40
  const prof = (profile || 'general').toLowerCase();
41
- if (prof === 'medical') {
42
- text = text.replace(/Patient Name:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Patient Name: $1');
41
+ if (prof === 'medical' || prof === 'general') {
42
+ text = text.replace(/(Patient\s+Name|Emergency\s+Contact|In\s+Case\s+of\s+Emergency|ICE(?:\s+Contact)?):\s*\r?\n+([A-Z\p{Lu}][A-Za-z0-9&.,'’ \t-]*?[A-Za-z0-9\p{Lu}\p{Ll}])(?=\r?\n|$)/gui, '$1: $2');
43
43
  } else if (prof === 'legal') {
44
- text = text.replace(/Defendant:\s*\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
44
+ text = text.replace(/Defendant:\s*\r?\n+([A-Z][a-zA-Z]+\s[A-Z][a-zA-Z]+)/g, 'Defendant: $1');
45
45
  }
46
46
  return text;
47
47
  }
@@ -1116,7 +1116,7 @@ function hydrateRegex(rule) {
1116
1116
  }
1117
1117
  });
1118
1118
  if (remaskedCount > 0) {
1119
- if (typeof showInPageToast === 'function') showInPageToast(`πŸ”’ Re-masked in DOM. Original tokens restored.`, "info");
1119
+ if (typeof showInPageToast === 'function') showInPageToast('Re-masked in DOM. Original tokens restored.', "info");
1120
1120
  document.querySelectorAll('.ps-toolbar-reveal, [id$="-reveal"]').forEach(b => {
1121
1121
  b.setAttribute('title', 'Reveal Original Data (Decrypted Locally)');
1122
1122
  b.classList.remove('ps-revealed-active');
@@ -1184,7 +1184,7 @@ function hydrateRegex(rule) {
1184
1184
  });
1185
1185
 
1186
1186
  if (restoredCount > 0) {
1187
- if (typeof showInPageToast === 'function') showInPageToast(`βœ… Decrypted Locally! Your data is now safe to copy.`, "success");
1187
+ if (typeof showInPageToast === 'function') showInPageToast('βœ“ Decrypted Locally in RAM. Safe to copy.', "success");
1188
1188
  document.querySelectorAll('.ps-toolbar-reveal, [id$="-reveal"]').forEach(b => {
1189
1189
  b.setAttribute('title', 'Hide / Re-mask Protected Data in DOM');
1190
1190
  b.classList.add('ps-revealed-active');
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/moxno/privacyscrubber-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.1.0",
9
+ "version": "2.2.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@privacyscrubber/mcp-server",
14
- "version": "2.1.0",
14
+ "version": "2.2.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -26,4 +26,4 @@
26
26
  ]
27
27
  }
28
28
  ]
29
- }
29
+ }