@orderful/droid 0.45.1 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/.claude-plugin/plugin.json +4 -1
  2. package/.github/workflows/claude-issue-agent.yml +1 -1
  3. package/CHANGELOG.md +6 -0
  4. package/dist/tools/pii/.claude-plugin/plugin.json +25 -0
  5. package/dist/tools/pii/TOOL.yaml +22 -0
  6. package/dist/tools/pii/agents/pii-scanner.md +85 -0
  7. package/dist/tools/pii/commands/pii.md +33 -0
  8. package/dist/tools/pii/skills/pii/SKILL.md +97 -0
  9. package/dist/tools/pii/skills/pii/references/supported-entities.md +90 -0
  10. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.d.ts +18 -0
  11. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.d.ts.map +1 -0
  12. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.ts +258 -0
  13. package/dist/tools/pii/skills/pii/scripts/presidio-init.d.ts +17 -0
  14. package/dist/tools/pii/skills/pii/scripts/presidio-init.d.ts.map +1 -0
  15. package/dist/tools/pii/skills/pii/scripts/presidio-init.ts +151 -0
  16. package/dist/tools/pii/skills/pii/scripts/presidio-redact.d.ts +21 -0
  17. package/dist/tools/pii/skills/pii/scripts/presidio-redact.d.ts.map +1 -0
  18. package/dist/tools/pii/skills/pii/scripts/presidio-redact.ts +294 -0
  19. package/dist/tools/pii/skills/pii/scripts/presidio.test.ts +444 -0
  20. package/package.json +1 -1
  21. package/src/tools/pii/.claude-plugin/plugin.json +25 -0
  22. package/src/tools/pii/TOOL.yaml +22 -0
  23. package/src/tools/pii/agents/pii-scanner.md +85 -0
  24. package/src/tools/pii/commands/pii.md +33 -0
  25. package/src/tools/pii/skills/pii/SKILL.md +97 -0
  26. package/src/tools/pii/skills/pii/references/supported-entities.md +90 -0
  27. package/src/tools/pii/skills/pii/scripts/presidio-analyze.ts +258 -0
  28. package/src/tools/pii/skills/pii/scripts/presidio-init.ts +151 -0
  29. package/src/tools/pii/skills/pii/scripts/presidio-redact.ts +294 -0
  30. package/src/tools/pii/skills/pii/scripts/presidio.test.ts +444 -0
@@ -0,0 +1,444 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { spawnSync } from 'child_process';
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+
7
+ /**
8
+ * Integration tests for presidio scripts.
9
+ *
10
+ * These tests validate argument parsing, error paths, and JSON output shape.
11
+ * They do NOT invoke Presidio/Python directly — all Python-dependent paths
12
+ * are gated by existsSync checks in the scripts, so tests run without Python.
13
+ */
14
+
15
+ const SCRIPTS_DIR = __dirname;
16
+
17
+ interface ScriptResult {
18
+ success: boolean;
19
+ already_existed?: boolean;
20
+ initialized?: boolean;
21
+ python_path?: string;
22
+ venv_path?: string;
23
+ entities?: Array<{ type: string; start: number; end: number; score: number; line: number; text?: string }>;
24
+ dry_run?: boolean;
25
+ original_path?: string;
26
+ output_path?: string;
27
+ entities_found?: number;
28
+ entities_redacted?: number;
29
+ redacted_text?: string;
30
+ error?: string;
31
+ init_required?: boolean;
32
+ }
33
+
34
+ function runScript(scriptName: string, args: string[]): ScriptResult {
35
+ const scriptPath = join(SCRIPTS_DIR, `${scriptName}.ts`);
36
+
37
+ const result = spawnSync('bun', ['run', scriptPath, ...args], {
38
+ encoding: 'utf-8',
39
+ cwd: process.cwd(),
40
+ });
41
+
42
+ if (result.error) {
43
+ return { success: false, error: result.error.message };
44
+ }
45
+
46
+ try {
47
+ return JSON.parse(result.stdout.trim());
48
+ } catch {
49
+ return {
50
+ success: false,
51
+ error: `Failed to parse output: ${result.stdout}\nStderr: ${result.stderr}`,
52
+ };
53
+ }
54
+ }
55
+
56
+ function createFakeVenvWithPython(tempHome: string, scriptContent: string): string {
57
+ const venvPath = join(tempHome, '.droid', 'runtimes', 'presidio');
58
+ const binDir = join(venvPath, 'bin');
59
+ mkdirSync(binDir, { recursive: true });
60
+ writeFileSync(join(binDir, 'python3'), scriptContent, { mode: 0o755 });
61
+ return venvPath;
62
+ }
63
+
64
+ // ─── presidio-init ───────────────────────────────────────────────────────────
65
+
66
+ describe('presidio-init', () => {
67
+ let tempHome: string;
68
+ let originalHome: string;
69
+
70
+ beforeEach(() => {
71
+ originalHome = process.env.HOME || '';
72
+ tempHome = mkdtempSync(join(tmpdir(), 'pii-test-home-'));
73
+ process.env.HOME = tempHome;
74
+ });
75
+
76
+ afterEach(() => {
77
+ process.env.HOME = originalHome;
78
+ try {
79
+ rmSync(tempHome, { recursive: true, force: true });
80
+ } catch {
81
+ // Ignore cleanup errors
82
+ }
83
+ });
84
+
85
+ it('returns already_existed:true when marker file and venv binary both exist', () => {
86
+ // Pre-create the venv structure
87
+ const venvPath = join(tempHome, '.droid', 'runtimes', 'presidio');
88
+ const binDir = join(venvPath, 'bin');
89
+ mkdirSync(binDir, { recursive: true });
90
+ writeFileSync(join(venvPath, '.droid-initialized'), new Date().toISOString());
91
+ writeFileSync(join(binDir, 'python3'), '#!/bin/bash\necho "Python 3.9.0"', { mode: 0o755 });
92
+
93
+ const result = spawnSync(
94
+ 'bun',
95
+ ['run', join(SCRIPTS_DIR, 'presidio-init.ts')],
96
+ {
97
+ encoding: 'utf-8',
98
+ env: { ...process.env, HOME: tempHome },
99
+ }
100
+ );
101
+
102
+ let parsed: ScriptResult;
103
+ try {
104
+ parsed = JSON.parse(result.stdout.trim());
105
+ } catch {
106
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
107
+ }
108
+
109
+ expect(parsed.success).toBe(true);
110
+ expect(parsed.already_existed).toBe(true);
111
+ });
112
+
113
+ it('returns success:false with clear error when python3 is not available', () => {
114
+ // Override PATH to have no python3
115
+ const result = spawnSync(
116
+ 'bun',
117
+ ['run', join(SCRIPTS_DIR, 'presidio-init.ts')],
118
+ {
119
+ encoding: 'utf-8',
120
+ env: { ...process.env, HOME: tempHome, PATH: '/nonexistent' },
121
+ }
122
+ );
123
+
124
+ let parsed: ScriptResult;
125
+ try {
126
+ parsed = JSON.parse(result.stdout.trim());
127
+ } catch {
128
+ // If Python is truly not found, the error may be in stderr
129
+ parsed = { success: false, error: 'python3 not found' };
130
+ }
131
+
132
+ expect(parsed.success).toBe(false);
133
+ expect(parsed.error).toBeDefined();
134
+ });
135
+ });
136
+
137
+ // ─── presidio-analyze ────────────────────────────────────────────────────────
138
+
139
+ describe('presidio-analyze', () => {
140
+ let tempDir: string;
141
+
142
+ beforeEach(() => {
143
+ tempDir = mkdtempSync(join(tmpdir(), 'pii-analyze-test-'));
144
+ });
145
+
146
+ afterEach(() => {
147
+ try {
148
+ rmSync(tempDir, { recursive: true, force: true });
149
+ } catch {
150
+ // Ignore
151
+ }
152
+ });
153
+
154
+ it('returns success:false with init_required when venv does not exist', () => {
155
+ const result = spawnSync(
156
+ 'bun',
157
+ ['run', join(SCRIPTS_DIR, 'presidio-analyze.ts'), '--file', join(tempDir, 'test.md')],
158
+ {
159
+ encoding: 'utf-8',
160
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
161
+ }
162
+ );
163
+
164
+ let parsed: ScriptResult;
165
+ try {
166
+ parsed = JSON.parse(result.stdout.trim());
167
+ } catch {
168
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
169
+ }
170
+
171
+ expect(parsed.success).toBe(false);
172
+ expect(parsed.init_required).toBe(true);
173
+ expect(parsed.error).toContain('presidio-init');
174
+ });
175
+
176
+ it('returns success:false when neither --file nor --text is provided', () => {
177
+ // Use a fake home so venv won't be found — still validates arg parsing
178
+ const result = spawnSync(
179
+ 'bun',
180
+ ['run', join(SCRIPTS_DIR, 'presidio-analyze.ts')],
181
+ {
182
+ encoding: 'utf-8',
183
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
184
+ }
185
+ );
186
+
187
+ let parsed: ScriptResult;
188
+ try {
189
+ parsed = JSON.parse(result.stdout.trim());
190
+ } catch {
191
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
192
+ }
193
+
194
+ // Either init_required or missing arg error — both are valid failures
195
+ expect(parsed.success).toBe(false);
196
+ expect(parsed.error).toBeDefined();
197
+ });
198
+
199
+ it('returns success:false when --file does not exist (and venv exists)', () => {
200
+ // Pre-create a fake venv so the script gets past the venv check
201
+ const fakeHome = join(tempDir, 'fake-home');
202
+ createFakeVenvWithPython(fakeHome, '#!/bin/bash\necho ""');
203
+
204
+ const nonExistentFile = join(tempDir, 'does-not-exist.md');
205
+
206
+ const result = spawnSync(
207
+ 'bun',
208
+ ['run', join(SCRIPTS_DIR, 'presidio-analyze.ts'), '--file', nonExistentFile],
209
+ {
210
+ encoding: 'utf-8',
211
+ env: { ...process.env, HOME: fakeHome },
212
+ }
213
+ );
214
+
215
+ let parsed: ScriptResult;
216
+ try {
217
+ parsed = JSON.parse(result.stdout.trim());
218
+ } catch {
219
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
220
+ }
221
+
222
+ expect(parsed.success).toBe(false);
223
+ expect(parsed.error).toContain('not found');
224
+ });
225
+
226
+ it('rejects invalid entity names before invoking Python', () => {
227
+ const fakeHome = join(tempDir, 'fake-home');
228
+ createFakeVenvWithPython(fakeHome, '#!/bin/bash\necho \'[]\'');
229
+
230
+ const result = spawnSync(
231
+ 'bun',
232
+ [
233
+ 'run',
234
+ join(SCRIPTS_DIR, 'presidio-analyze.ts'),
235
+ '--text',
236
+ 'Call me at 555-1234',
237
+ '--entities',
238
+ 'EMAIL_ADDRESS,__import__("os").system("whoami")',
239
+ ],
240
+ {
241
+ encoding: 'utf-8',
242
+ env: { ...process.env, HOME: fakeHome },
243
+ }
244
+ );
245
+
246
+ const parsed = JSON.parse(result.stdout.trim()) as ScriptResult;
247
+ expect(parsed.success).toBe(false);
248
+ expect(parsed.error).toContain('Invalid entity type');
249
+ });
250
+
251
+ it('does not include raw text in analyzer output entities', () => {
252
+ const fakeHome = join(tempDir, 'fake-home-no-text');
253
+ createFakeVenvWithPython(
254
+ fakeHome,
255
+ `#!/bin/bash
256
+ if grep -q "'text': text\\[r.start:r.end\\]" "$1"; then
257
+ echo '[{"type":"EMAIL_ADDRESS","start":11,"end":27,"score":0.99,"text":"jane@example.com"}]'
258
+ else
259
+ echo '[{"type":"EMAIL_ADDRESS","start":11,"end":27,"score":0.99}]'
260
+ fi`,
261
+ );
262
+
263
+ const result = spawnSync(
264
+ 'bun',
265
+ ['run', join(SCRIPTS_DIR, 'presidio-analyze.ts'), '--text', 'Contact me: jane@example.com'],
266
+ {
267
+ encoding: 'utf-8',
268
+ env: { ...process.env, HOME: fakeHome },
269
+ }
270
+ );
271
+
272
+ const parsed = JSON.parse(result.stdout.trim()) as ScriptResult;
273
+ expect(parsed.success).toBe(true);
274
+ expect(parsed.entities?.[0]?.type).toBe('EMAIL_ADDRESS');
275
+ expect(parsed.entities?.[0]).not.toHaveProperty('text');
276
+ });
277
+ });
278
+
279
+ // ─── presidio-redact ─────────────────────────────────────────────────────────
280
+
281
+ describe('presidio-redact', () => {
282
+ let tempDir: string;
283
+
284
+ beforeEach(() => {
285
+ tempDir = mkdtempSync(join(tmpdir(), 'pii-redact-test-'));
286
+ });
287
+
288
+ afterEach(() => {
289
+ try {
290
+ rmSync(tempDir, { recursive: true, force: true });
291
+ } catch {
292
+ // Ignore
293
+ }
294
+ });
295
+
296
+ it('returns success:false when --file is missing', () => {
297
+ const result = spawnSync(
298
+ 'bun',
299
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts')],
300
+ {
301
+ encoding: 'utf-8',
302
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
303
+ }
304
+ );
305
+
306
+ let parsed: ScriptResult;
307
+ try {
308
+ parsed = JSON.parse(result.stdout.trim());
309
+ } catch {
310
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
311
+ }
312
+
313
+ expect(parsed.success).toBe(false);
314
+ // Either init_required or missing --file error
315
+ expect(parsed.error).toBeDefined();
316
+ });
317
+
318
+ it('returns success:false with init_required when venv does not exist', () => {
319
+ const testFile = join(tempDir, 'test.md');
320
+ writeFileSync(testFile, 'Hello, my email is test@example.com');
321
+
322
+ const result = spawnSync(
323
+ 'bun',
324
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile],
325
+ {
326
+ encoding: 'utf-8',
327
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
328
+ }
329
+ );
330
+
331
+ let parsed: ScriptResult;
332
+ try {
333
+ parsed = JSON.parse(result.stdout.trim());
334
+ } catch {
335
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
336
+ }
337
+
338
+ expect(parsed.success).toBe(false);
339
+ expect(parsed.init_required).toBe(true);
340
+ expect(parsed.error).toContain('presidio-init');
341
+ });
342
+
343
+ it('does not write output file with --dry-run (venv missing path)', () => {
344
+ const testFile = join(tempDir, 'test.md');
345
+ writeFileSync(testFile, 'Hello, my email is test@example.com');
346
+ const expectedOutput = join(tempDir, 'test-redacted.md');
347
+
348
+ const result = spawnSync(
349
+ 'bun',
350
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile, '--dry-run'],
351
+ {
352
+ encoding: 'utf-8',
353
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
354
+ }
355
+ );
356
+
357
+ // With no venv, it will fail before writing
358
+ expect(existsSync(expectedOutput)).toBe(false);
359
+
360
+ let parsed: ScriptResult;
361
+ try {
362
+ parsed = JSON.parse(result.stdout.trim());
363
+ } catch {
364
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
365
+ }
366
+ expect(parsed.success).toBe(false);
367
+ expect(parsed.init_required).toBe(true);
368
+ });
369
+
370
+ it('correctly parses --output path argument', () => {
371
+ const testFile = join(tempDir, 'source.md');
372
+ writeFileSync(testFile, 'Contact: jane@example.com');
373
+ const customOutput = join(tempDir, 'custom-output.md');
374
+
375
+ // Without venv this will fail, but we can check the argument is parsed
376
+ const result = spawnSync(
377
+ 'bun',
378
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile, '--output', customOutput],
379
+ {
380
+ encoding: 'utf-8',
381
+ env: { ...process.env, HOME: join(tempDir, 'nonexistent-home') },
382
+ }
383
+ );
384
+
385
+ let parsed: ScriptResult;
386
+ try {
387
+ parsed = JSON.parse(result.stdout.trim());
388
+ } catch {
389
+ parsed = { success: false, error: `Parse error: ${result.stdout}` };
390
+ }
391
+
392
+ // With no venv this will fail with init_required
393
+ expect(parsed.success).toBe(false);
394
+ expect(parsed.init_required).toBe(true);
395
+ // Confirm the output file was NOT created
396
+ expect(existsSync(customOutput)).toBe(false);
397
+ });
398
+
399
+ it('omits redacted_text in non-dry-run output', () => {
400
+ const fakeHome = join(tempDir, 'fake-home-no-redacted-text');
401
+ createFakeVenvWithPython(fakeHome, '#!/bin/bash\necho \'{"redacted_text":"masked text","entities_found":1,"entities_redacted":1}\'');
402
+
403
+ const testFile = join(tempDir, 'source.md');
404
+ const outputFile = join(tempDir, 'source-redacted.md');
405
+ writeFileSync(testFile, 'Contact: jane@example.com');
406
+
407
+ const result = spawnSync(
408
+ 'bun',
409
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile],
410
+ {
411
+ encoding: 'utf-8',
412
+ env: { ...process.env, HOME: fakeHome },
413
+ }
414
+ );
415
+
416
+ const parsed = JSON.parse(result.stdout.trim()) as ScriptResult;
417
+ expect(parsed.success).toBe(true);
418
+ expect(parsed.dry_run).toBe(false);
419
+ expect(parsed.redacted_text).toBeUndefined();
420
+ expect(parsed.output_path).toBe(outputFile);
421
+ expect(existsSync(outputFile)).toBe(true);
422
+ });
423
+
424
+ it('rejects unsupported entity names', () => {
425
+ const fakeHome = join(tempDir, 'fake-home');
426
+ createFakeVenvWithPython(fakeHome, '#!/bin/bash\necho \'{"redacted_text":"ok","entities_found":0,"entities_redacted":0}\'');
427
+
428
+ const testFile = join(tempDir, 'source.md');
429
+ writeFileSync(testFile, 'Contact: jane@example.com');
430
+
431
+ const result = spawnSync(
432
+ 'bun',
433
+ ['run', join(SCRIPTS_DIR, 'presidio-redact.ts'), '--file', testFile, '--entities', 'NOT_A_REAL_ENTITY'],
434
+ {
435
+ encoding: 'utf-8',
436
+ env: { ...process.env, HOME: fakeHome },
437
+ }
438
+ );
439
+
440
+ const parsed = JSON.parse(result.stdout.trim()) as ScriptResult;
441
+ expect(parsed.success).toBe(false);
442
+ expect(parsed.error).toContain('Unsupported entity type');
443
+ });
444
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orderful/droid",
3
- "version": "0.45.1",
3
+ "version": "0.46.0",
4
4
  "description": "AI workflow toolkit for sharing skills, commands, and agents across the team",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "droid-pii",
3
+ "version": "0.1.0",
4
+ "description": "Detect and redact PII (personally identifiable information) in text files. Powered by Microsoft Presidio with a bundled Python venv — zero external dependencies after first run.",
5
+ "author": {
6
+ "name": "Orderful",
7
+ "url": "https://github.com/orderful"
8
+ },
9
+ "repository": "https://github.com/orderful/droid",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "droid",
13
+ "ai",
14
+ "pii"
15
+ ],
16
+ "skills": [
17
+ "./skills/pii/SKILL.md"
18
+ ],
19
+ "commands": [
20
+ "./commands/pii.md"
21
+ ],
22
+ "agents": [
23
+ "./agents/pii-scanner.md"
24
+ ]
25
+ }
@@ -0,0 +1,22 @@
1
+ name: pii
2
+ description: "Detect and redact PII (personally identifiable information) in text files. Powered by Microsoft Presidio with a bundled Python venv — zero external dependencies after first run."
3
+ version: 0.1.0
4
+ status: beta
5
+
6
+ includes:
7
+ skills:
8
+ - name: pii
9
+ required: true
10
+ commands:
11
+ - name: pii
12
+ is_alias: false
13
+ agents:
14
+ - pii-scanner
15
+
16
+ dependencies: []
17
+
18
+ prerequisites:
19
+ - name: python3
20
+ description: "Python 3.8+ required to run the Presidio venv"
21
+ check: "python3 --version"
22
+ install_hint: "Install Python 3 from python.org or via: brew install python3"
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: pii-scanner
3
+ description: "Isolated PII analysis agent. Runs presidio-analyze.ts in a contained context so raw entity values never appear in the parent conversation. Use PROACTIVELY when the pii skill delegates scan operations."
4
+ tools:
5
+ - Bash
6
+ color: orange
7
+ ---
8
+
9
+ You are a PII scanning agent. Your sole job is to run `presidio-analyze.ts` on a file or text and return a structured summary — without leaking raw PII values into the conversation.
10
+
11
+ ## Inputs
12
+
13
+ You will receive:
14
+
15
+ 1. `file_path` — absolute path to the file to scan (preferred), OR
16
+ 2. `text_content` — inline text to scan (for small strings only)
17
+ 3. `entities` (optional) — comma-separated list of entity types to filter (e.g. `EMAIL_ADDRESS,PHONE_NUMBER`)
18
+ 4. `venv_path` (optional) — override for the Presidio venv path (default: `~/.droid/runtimes/presidio/`)
19
+
20
+ ## Rules
21
+
22
+ - **Never echo raw PII values** in your output — return entity types, counts, and line numbers only
23
+ - Make exactly one Bash call to `presidio-analyze.ts`
24
+ - Parse the JSON result and return only the structured summary
25
+ - If the script returns `init_required: true`, stop and tell the parent skill to run `presidio-init.ts` first
26
+ - If the file does not exist, return a clear error
27
+
28
+ ## Procedure
29
+
30
+ 1. Build the command:
31
+ ```bash
32
+ droid exec pii presidio-analyze --file <file_path> [--entities <types>]
33
+ ```
34
+ (Use `--text` only for inline strings under ~1000 characters)
35
+
36
+ 2. Parse the JSON output from the script
37
+
38
+ 3. From the `entities` array, compute:
39
+ - `total_entities`: total count
40
+ - `by_type`: entity type → count map
41
+ - `lines_affected`: sorted unique list of line numbers
42
+ - `sample_lines`: up to 5 line numbers with the entity types found on each line
43
+
44
+ 4. Return the structured summary (see Output Format below)
45
+
46
+ ## Output Format
47
+
48
+ Return JSON:
49
+
50
+ ```json
51
+ {
52
+ "file": "/path/to/file.md",
53
+ "total_entities": 3,
54
+ "by_type": {
55
+ "EMAIL_ADDRESS": 2,
56
+ "PHONE_NUMBER": 1
57
+ },
58
+ "lines_affected": [4, 8, 12],
59
+ "sample_lines": [
60
+ { "line": 4, "types": ["EMAIL_ADDRESS"] },
61
+ { "line": 8, "types": ["PHONE_NUMBER"] },
62
+ { "line": 12, "types": ["EMAIL_ADDRESS"] }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ If no entities are found:
68
+ ```json
69
+ {
70
+ "file": "/path/to/file.md",
71
+ "total_entities": 0,
72
+ "by_type": {},
73
+ "lines_affected": [],
74
+ "sample_lines": []
75
+ }
76
+ ```
77
+
78
+ If an error occurs:
79
+ ```json
80
+ {
81
+ "file": "/path/to/file.md",
82
+ "error": "...",
83
+ "init_required": true
84
+ }
85
+ ```
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: pii
3
+ description: "Detect and redact PII (personally identifiable information) in files and directories. Powered by Microsoft Presidio."
4
+ argument-hint: "[scan | redact] {file|dir} [--entities TYPES] [--output PATH] [--dry-run] [--mask]"
5
+ ---
6
+
7
+ # /pii
8
+
9
+ **User invoked:** `/pii $ARGUMENTS`
10
+
11
+ **Your task:** Invoke the **pii skill** with these arguments.
12
+
13
+ ## Examples
14
+
15
+ - `/pii scan transcript.md`
16
+ - `/pii scan ./meeting-notes/`
17
+ - `/pii redact transcript.md --output transcript-clean.md`
18
+ - `/pii redact transcript.md --dry-run`
19
+ - `/pii redact transcript.md --entities PHONE_NUMBER,EMAIL_ADDRESS`
20
+
21
+ ## Quick Reference
22
+
23
+ ```bash
24
+ /pii scan {file} # Scan file for PII
25
+ /pii scan {dir} # Scan directory recursively
26
+ /pii redact {file} # Redact PII (writes {file}-redacted.{ext})
27
+ /pii redact {file} --output {out} # Redact to specific output path
28
+ /pii redact {file} --dry-run # Preview redactions without writing
29
+ /pii redact {file} --entities TYPES # Redact only specific entity types
30
+ /pii redact {file} --mask # Use *** instead of <ENTITY_TYPE>
31
+ ```
32
+
33
+ See the **pii skill** for full behaviour, procedure, and supported entity types.