@orderful/droid 0.45.1 → 0.47.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/.claude-plugin/plugin.json +4 -1
- package/.github/workflows/claude-issue-agent.yml +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/tools/pii/.claude-plugin/plugin.json +25 -0
- package/dist/tools/pii/TOOL.yaml +22 -0
- package/dist/tools/pii/agents/pii-scanner.md +85 -0
- package/dist/tools/pii/commands/pii.md +33 -0
- package/dist/tools/pii/skills/pii/SKILL.md +97 -0
- package/dist/tools/pii/skills/pii/references/supported-entities.md +90 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-analyze.d.ts +18 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-analyze.d.ts.map +1 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-analyze.ts +258 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-init.d.ts +17 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-init.d.ts.map +1 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-init.ts +151 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-redact.d.ts +21 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-redact.d.ts.map +1 -0
- package/dist/tools/pii/skills/pii/scripts/presidio-redact.ts +294 -0
- package/dist/tools/pii/skills/pii/scripts/presidio.test.ts +444 -0
- package/dist/tools/project/.claude-plugin/plugin.json +1 -1
- package/dist/tools/project/TOOL.yaml +2 -2
- package/dist/tools/project/skills/project/SKILL.md +4 -3
- package/dist/tools/project/skills/project/references/changelog.md +10 -21
- package/dist/tools/project/skills/project/references/creating.md +12 -2
- package/dist/tools/project/skills/project/references/loading.md +2 -1
- package/dist/tools/project/skills/project/references/templates.md +115 -71
- package/dist/tools/project/skills/project/references/updating.md +78 -4
- package/package.json +1 -1
- package/src/tools/pii/.claude-plugin/plugin.json +25 -0
- package/src/tools/pii/TOOL.yaml +22 -0
- package/src/tools/pii/agents/pii-scanner.md +85 -0
- package/src/tools/pii/commands/pii.md +33 -0
- package/src/tools/pii/skills/pii/SKILL.md +97 -0
- package/src/tools/pii/skills/pii/references/supported-entities.md +90 -0
- package/src/tools/pii/skills/pii/scripts/presidio-analyze.ts +258 -0
- package/src/tools/pii/skills/pii/scripts/presidio-init.ts +151 -0
- package/src/tools/pii/skills/pii/scripts/presidio-redact.ts +294 -0
- package/src/tools/pii/skills/pii/scripts/presidio.test.ts +444 -0
- package/src/tools/project/.claude-plugin/plugin.json +1 -1
- package/src/tools/project/TOOL.yaml +2 -2
- package/src/tools/project/skills/project/SKILL.md +4 -3
- package/src/tools/project/skills/project/references/changelog.md +10 -21
- package/src/tools/project/skills/project/references/creating.md +12 -2
- package/src/tools/project/skills/project/references/loading.md +2 -1
- package/src/tools/project/skills/project/references/templates.md +115 -71
- package/src/tools/project/skills/project/references/updating.md +78 -4
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* presidio-redact
|
|
4
|
+
*
|
|
5
|
+
* Redact PII in a file using Presidio.
|
|
6
|
+
* Shells out to the bundled Python venv.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun run presidio-redact.ts --file transcript.md
|
|
10
|
+
* bun run presidio-redact.ts --file transcript.md --output clean.md
|
|
11
|
+
* bun run presidio-redact.ts --file transcript.md --dry-run
|
|
12
|
+
* bun run presidio-redact.ts --file transcript.md --entities EMAIL_ADDRESS,PHONE_NUMBER
|
|
13
|
+
* bun run presidio-redact.ts --file transcript.md --mask
|
|
14
|
+
*
|
|
15
|
+
* Output (JSON):
|
|
16
|
+
* { "success": true, "dry_run": false, "original_path": "...", "output_path": "...", "entities_found": 3, "entities_redacted": 3 }
|
|
17
|
+
* { "success": true, "dry_run": true, "original_path": "...", "entities_found": 3, "entities_redacted": 3, "redacted_text": "..." }
|
|
18
|
+
* { "success": false, "error": "..." }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execSync } from 'child_process';
|
|
22
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
|
|
23
|
+
import { join, dirname, basename, extname } from 'path';
|
|
24
|
+
import { tmpdir } from 'os';
|
|
25
|
+
|
|
26
|
+
const VENV_PATH = join(process.env.HOME || '', '.droid', 'runtimes', 'presidio');
|
|
27
|
+
const VENV_PYTHON = join(VENV_PATH, 'bin', 'python3');
|
|
28
|
+
const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
29
|
+
const ENTITY_NAME_PATTERN = /^[A-Z0-9_]+$/;
|
|
30
|
+
const SUPPORTED_ENTITIES = new Set([
|
|
31
|
+
'PERSON',
|
|
32
|
+
'EMAIL_ADDRESS',
|
|
33
|
+
'PHONE_NUMBER',
|
|
34
|
+
'CREDIT_CARD',
|
|
35
|
+
'IBAN_CODE',
|
|
36
|
+
'IP_ADDRESS',
|
|
37
|
+
'LOCATION',
|
|
38
|
+
'DATE_TIME',
|
|
39
|
+
'NRP',
|
|
40
|
+
'MEDICAL_LICENSE',
|
|
41
|
+
'URL',
|
|
42
|
+
'CRYPTO',
|
|
43
|
+
'US_SSN',
|
|
44
|
+
'US_PASSPORT',
|
|
45
|
+
'US_ITIN',
|
|
46
|
+
'US_DRIVER_LICENSE',
|
|
47
|
+
'US_BANK_NUMBER',
|
|
48
|
+
'UK_NHS',
|
|
49
|
+
'ES_NIF',
|
|
50
|
+
'IT_FISCAL_CODE',
|
|
51
|
+
'IT_DRIVER_LICENSE',
|
|
52
|
+
'IT_VAT_CODE',
|
|
53
|
+
'IT_PASSPORT',
|
|
54
|
+
'IT_IDENTITY_CARD',
|
|
55
|
+
'PL_PESEL',
|
|
56
|
+
'SG_NRIC_FIN',
|
|
57
|
+
'AU_ABN',
|
|
58
|
+
'AU_ACN',
|
|
59
|
+
'AU_TFN',
|
|
60
|
+
'AU_MEDICARE',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
interface RedactResult {
|
|
64
|
+
success: boolean;
|
|
65
|
+
dry_run?: boolean;
|
|
66
|
+
original_path?: string;
|
|
67
|
+
output_path?: string;
|
|
68
|
+
entities_found?: number;
|
|
69
|
+
entities_redacted?: number;
|
|
70
|
+
redacted_text?: string;
|
|
71
|
+
error?: string;
|
|
72
|
+
init_required?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ParsedArgs {
|
|
76
|
+
file?: string;
|
|
77
|
+
output?: string;
|
|
78
|
+
dryRun: boolean;
|
|
79
|
+
entities?: string[];
|
|
80
|
+
mask: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseArgs(args: string[]): ParsedArgs {
|
|
84
|
+
const result: ParsedArgs = { dryRun: false, mask: false };
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < args.length; i++) {
|
|
87
|
+
const arg = args[i];
|
|
88
|
+
if (arg === '--file' && args[i + 1]) {
|
|
89
|
+
result.file = args[++i];
|
|
90
|
+
} else if (arg === '--output' && args[i + 1]) {
|
|
91
|
+
result.output = args[++i];
|
|
92
|
+
} else if (arg === '--dry-run') {
|
|
93
|
+
result.dryRun = true;
|
|
94
|
+
} else if (arg === '--entities' && args[i + 1]) {
|
|
95
|
+
result.entities = args[++i].split(',').map(e => e.trim());
|
|
96
|
+
} else if (arg === '--mask') {
|
|
97
|
+
result.mask = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function defaultOutputPath(filePath: string): string {
|
|
105
|
+
const dir = dirname(filePath);
|
|
106
|
+
const ext = extname(filePath);
|
|
107
|
+
const base = basename(filePath, ext);
|
|
108
|
+
return join(dir, `${base}-redacted${ext}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateEntities(entities: string[] | undefined): string | undefined {
|
|
112
|
+
if (!entities || entities.length === 0) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const entity of entities) {
|
|
117
|
+
if (!ENTITY_NAME_PATTERN.test(entity)) {
|
|
118
|
+
return `Invalid entity type: ${entity}. Allowed pattern: ${ENTITY_NAME_PATTERN.source}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!SUPPORTED_ENTITIES.has(entity)) {
|
|
122
|
+
return `Unsupported entity type: ${entity}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function run(cmd: string): { ok: boolean; stdout: string; stderr: string } {
|
|
130
|
+
try {
|
|
131
|
+
const output = execSync(cmd, {
|
|
132
|
+
encoding: 'utf-8',
|
|
133
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
134
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
135
|
+
});
|
|
136
|
+
return { ok: true, stdout: output, stderr: '' };
|
|
137
|
+
} catch (err: unknown) {
|
|
138
|
+
const error = err as { stdout?: string; stderr?: string; message?: string };
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
stdout: error.stdout || '',
|
|
142
|
+
stderr: error.stderr || error.message || 'Unknown error',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function presidioRedact(parsed: ParsedArgs): RedactResult {
|
|
148
|
+
// Validate venv
|
|
149
|
+
if (!existsSync(VENV_PYTHON)) {
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
error: 'Presidio venv not found. Run presidio-init.ts first.',
|
|
153
|
+
init_required: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Validate required args
|
|
158
|
+
if (!parsed.file) {
|
|
159
|
+
return { success: false, error: '--file is required.' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!existsSync(parsed.file)) {
|
|
163
|
+
return { success: false, error: `File not found: ${parsed.file}` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const entitiesError = validateEntities(parsed.entities);
|
|
167
|
+
if (entitiesError) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: entitiesError,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let sourceText: string;
|
|
175
|
+
try {
|
|
176
|
+
sourceText = readFileSync(parsed.file, 'utf-8');
|
|
177
|
+
} catch (err: unknown) {
|
|
178
|
+
const e = err as { message?: string };
|
|
179
|
+
return { success: false, error: `Failed to read file: ${e.message}` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Build entity filter
|
|
183
|
+
const entitiesArg = parsed.entities && parsed.entities.length > 0
|
|
184
|
+
? `entities=[${parsed.entities.map(e => `"${e}"`).join(', ')}]`
|
|
185
|
+
: '';
|
|
186
|
+
|
|
187
|
+
const pythonScript = `
|
|
188
|
+
import sys, json
|
|
189
|
+
from presidio_analyzer import AnalyzerEngine
|
|
190
|
+
from presidio_anonymizer import AnonymizerEngine
|
|
191
|
+
from presidio_anonymizer.entities import OperatorConfig
|
|
192
|
+
|
|
193
|
+
analyzer = AnalyzerEngine()
|
|
194
|
+
anonymizer = AnonymizerEngine()
|
|
195
|
+
text = ${JSON.stringify(sourceText)}
|
|
196
|
+
|
|
197
|
+
# Analyze
|
|
198
|
+
results = analyzer.analyze(text=text, language='en'${entitiesArg ? ', ' + entitiesArg : ''})
|
|
199
|
+
entities_found = len(results)
|
|
200
|
+
|
|
201
|
+
# Redact
|
|
202
|
+
if ${parsed.mask ? 'True' : 'False'}:
|
|
203
|
+
redacted_text = text
|
|
204
|
+
for r in sorted(results, key=lambda e: e.start, reverse=True):
|
|
205
|
+
replacement = '*' * (r.end - r.start)
|
|
206
|
+
redacted_text = redacted_text[:r.start] + replacement + redacted_text[r.end:]
|
|
207
|
+
else:
|
|
208
|
+
operators = {r.entity_type: OperatorConfig('replace', {'new_value': f'<{r.entity_type}>'}) for r in results}
|
|
209
|
+
anonymized = anonymizer.anonymize(text=text, analyzer_results=results, operators=operators)
|
|
210
|
+
redacted_text = anonymized.text
|
|
211
|
+
|
|
212
|
+
print(json.dumps({
|
|
213
|
+
'redacted_text': redacted_text,
|
|
214
|
+
'entities_found': entities_found,
|
|
215
|
+
'entities_redacted': entities_found
|
|
216
|
+
}))
|
|
217
|
+
`.trim();
|
|
218
|
+
|
|
219
|
+
// Write tmp script
|
|
220
|
+
const tmpDir = tmpdir();
|
|
221
|
+
const tmpScript = join(tmpDir, `pii-redact-${Date.now()}.py`);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
225
|
+
writeFileSync(tmpScript, pythonScript, 'utf-8');
|
|
226
|
+
} catch (err: unknown) {
|
|
227
|
+
const e = err as { message?: string };
|
|
228
|
+
return { success: false, error: `Failed to write temp script: ${e.message}` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const result = run(`"${VENV_PYTHON}" "${tmpScript}"`);
|
|
233
|
+
|
|
234
|
+
if (!result.ok) {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
error: `Presidio redaction failed: ${result.stderr}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let pyResult: { redacted_text: string; entities_found: number; entities_redacted: number };
|
|
242
|
+
try {
|
|
243
|
+
pyResult = JSON.parse(result.stdout.trim());
|
|
244
|
+
} catch {
|
|
245
|
+
return { success: false, error: `Failed to parse Presidio output: ${result.stdout}` };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const outputPath = parsed.dryRun
|
|
249
|
+
? undefined
|
|
250
|
+
: (parsed.output || defaultOutputPath(parsed.file));
|
|
251
|
+
|
|
252
|
+
// Write output file unless dry run
|
|
253
|
+
if (!parsed.dryRun && outputPath) {
|
|
254
|
+
try {
|
|
255
|
+
writeFileSync(outputPath, pyResult.redacted_text, 'utf-8');
|
|
256
|
+
} catch (err: unknown) {
|
|
257
|
+
const e = err as { message?: string };
|
|
258
|
+
return { success: false, error: `Failed to write output file: ${e.message}` };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const baseResult: RedactResult = {
|
|
263
|
+
success: true,
|
|
264
|
+
dry_run: parsed.dryRun,
|
|
265
|
+
original_path: parsed.file,
|
|
266
|
+
output_path: outputPath,
|
|
267
|
+
entities_found: pyResult.entities_found,
|
|
268
|
+
entities_redacted: pyResult.entities_redacted,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
if (parsed.dryRun) {
|
|
272
|
+
baseResult.redacted_text = pyResult.redacted_text;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return baseResult;
|
|
276
|
+
} finally {
|
|
277
|
+
// Clean up tmp file
|
|
278
|
+
try {
|
|
279
|
+
unlinkSync(tmpScript);
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore cleanup errors
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Main
|
|
287
|
+
const args = process.argv.slice(2);
|
|
288
|
+
const parsed = parseArgs(args);
|
|
289
|
+
const result = presidioRedact(parsed);
|
|
290
|
+
console.log(JSON.stringify(result, null, 2));
|
|
291
|
+
|
|
292
|
+
if (!result.success) {
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|