@orderful/droid 0.45.0 → 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 (49) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +8 -2
  3. package/.github/workflows/claude-issue-agent.yml +1 -2
  4. package/CHANGELOG.md +14 -0
  5. package/dist/tools/droid/.claude-plugin/plugin.json +1 -1
  6. package/dist/tools/droid/TOOL.yaml +1 -1
  7. package/dist/tools/droid/skills/droid/SKILL.md +1 -0
  8. package/dist/tools/droid/skills/droid-bootstrap/SKILL.md +1 -0
  9. package/dist/tools/edi-schema/.claude-plugin/plugin.json +25 -0
  10. package/dist/tools/edi-schema/TOOL.yaml +29 -0
  11. package/dist/tools/edi-schema/agents/edi-schema-agent.md +97 -0
  12. package/dist/tools/edi-schema/commands/edi-schema.md +33 -0
  13. package/dist/tools/edi-schema/skills/edi-schema/SKILL.md +86 -0
  14. package/dist/tools/pii/.claude-plugin/plugin.json +25 -0
  15. package/dist/tools/pii/TOOL.yaml +22 -0
  16. package/dist/tools/pii/agents/pii-scanner.md +85 -0
  17. package/dist/tools/pii/commands/pii.md +33 -0
  18. package/dist/tools/pii/skills/pii/SKILL.md +97 -0
  19. package/dist/tools/pii/skills/pii/references/supported-entities.md +90 -0
  20. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.d.ts +18 -0
  21. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.d.ts.map +1 -0
  22. package/dist/tools/pii/skills/pii/scripts/presidio-analyze.ts +258 -0
  23. package/dist/tools/pii/skills/pii/scripts/presidio-init.d.ts +17 -0
  24. package/dist/tools/pii/skills/pii/scripts/presidio-init.d.ts.map +1 -0
  25. package/dist/tools/pii/skills/pii/scripts/presidio-init.ts +151 -0
  26. package/dist/tools/pii/skills/pii/scripts/presidio-redact.d.ts +21 -0
  27. package/dist/tools/pii/skills/pii/scripts/presidio-redact.d.ts.map +1 -0
  28. package/dist/tools/pii/skills/pii/scripts/presidio-redact.ts +294 -0
  29. package/dist/tools/pii/skills/pii/scripts/presidio.test.ts +444 -0
  30. package/package.json +1 -1
  31. package/src/tools/droid/.claude-plugin/plugin.json +1 -1
  32. package/src/tools/droid/TOOL.yaml +1 -1
  33. package/src/tools/droid/skills/droid/SKILL.md +1 -0
  34. package/src/tools/droid/skills/droid-bootstrap/SKILL.md +1 -0
  35. package/src/tools/edi-schema/.claude-plugin/plugin.json +25 -0
  36. package/src/tools/edi-schema/TOOL.yaml +29 -0
  37. package/src/tools/edi-schema/agents/edi-schema-agent.md +97 -0
  38. package/src/tools/edi-schema/commands/edi-schema.md +33 -0
  39. package/src/tools/edi-schema/skills/edi-schema/SKILL.md +86 -0
  40. package/src/tools/pii/.claude-plugin/plugin.json +25 -0
  41. package/src/tools/pii/TOOL.yaml +22 -0
  42. package/src/tools/pii/agents/pii-scanner.md +85 -0
  43. package/src/tools/pii/commands/pii.md +33 -0
  44. package/src/tools/pii/skills/pii/SKILL.md +97 -0
  45. package/src/tools/pii/skills/pii/references/supported-entities.md +90 -0
  46. package/src/tools/pii/skills/pii/scripts/presidio-analyze.ts +258 -0
  47. package/src/tools/pii/skills/pii/scripts/presidio-init.ts +151 -0
  48. package/src/tools/pii/skills/pii/scripts/presidio-redact.ts +294 -0
  49. package/src/tools/pii/skills/pii/scripts/presidio.test.ts +444 -0
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * presidio-init
4
+ *
5
+ * One-time bootstrap for the Presidio Python venv.
6
+ * Idempotent: no-op if already initialised (marker file check + venv binary check).
7
+ *
8
+ * Usage:
9
+ * bun run presidio-init.ts
10
+ *
11
+ * Output (JSON):
12
+ * { "success": true, "already_existed": true }
13
+ * { "success": true, "initialized": true, "python_path": "...", "venv_path": "..." }
14
+ * { "success": false, "error": "..." }
15
+ */
16
+
17
+ import { execSync } from 'child_process';
18
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
19
+ import { join } from 'path';
20
+
21
+ const VENV_PATH = join(process.env.HOME || '', '.droid', 'runtimes', 'presidio');
22
+ const MARKER_FILE = join(VENV_PATH, '.droid-initialized');
23
+ const VENV_PYTHON = join(VENV_PATH, 'bin', 'python3');
24
+
25
+ interface InitResult {
26
+ success: boolean;
27
+ already_existed?: boolean;
28
+ initialized?: boolean;
29
+ python_path?: string;
30
+ venv_path?: string;
31
+ error?: string;
32
+ }
33
+
34
+ function run(
35
+ cmd: string,
36
+ opts: { visible?: boolean; cwd?: string } = {}
37
+ ): { ok: boolean; output: string } {
38
+ try {
39
+ const output = execSync(cmd, {
40
+ cwd: opts.cwd,
41
+ encoding: 'utf-8',
42
+ stdio: opts.visible ? 'inherit' : ['pipe', 'pipe', 'pipe'],
43
+ });
44
+ return { ok: true, output: typeof output === 'string' ? output.trim() : '' };
45
+ } catch (err: unknown) {
46
+ const error = err as { stderr?: string; message?: string };
47
+ return { ok: false, output: error.stderr || error.message || 'Unknown error' };
48
+ }
49
+ }
50
+
51
+ function checkPythonVersion(): { ok: boolean; version?: string; error?: string } {
52
+ const result = run('python3 --version');
53
+ if (!result.ok) {
54
+ return { ok: false, error: 'python3 not found. Install Python 3.8+ from python.org or via: brew install python3' };
55
+ }
56
+
57
+ const match = result.output.match(/Python (\d+)\.(\d+)/);
58
+ if (!match) {
59
+ return { ok: false, error: `Could not parse Python version from: ${result.output}` };
60
+ }
61
+
62
+ const major = parseInt(match[1], 10);
63
+ const minor = parseInt(match[2], 10);
64
+
65
+ if (major < 3 || (major === 3 && minor < 8)) {
66
+ return {
67
+ ok: false,
68
+ error: `Python 3.8+ required, found ${result.output.trim()}. Upgrade via: brew install python3`,
69
+ };
70
+ }
71
+
72
+ return { ok: true, version: result.output.trim() };
73
+ }
74
+
75
+ function presidioInit(): InitResult {
76
+ // Fast path: marker file + binary both exist
77
+ if (existsSync(MARKER_FILE) && existsSync(VENV_PYTHON)) {
78
+ return { success: true, already_existed: true };
79
+ }
80
+
81
+ // Check Python version before attempting setup
82
+ const pythonCheck = checkPythonVersion();
83
+ if (!pythonCheck.ok) {
84
+ return { success: false, error: pythonCheck.error };
85
+ }
86
+
87
+ // Create parent directory
88
+ try {
89
+ mkdirSync(join(process.env.HOME || '', '.droid', 'runtimes'), { recursive: true });
90
+ } catch (err: unknown) {
91
+ const e = err as { message?: string };
92
+ return { success: false, error: `Failed to create runtimes directory: ${e.message}` };
93
+ }
94
+
95
+ // Step 1: Create venv
96
+ console.error('[pii] Creating Python venv (first run — this takes ~2–3 min)...');
97
+ const venvResult = run(`python3 -m venv "${VENV_PATH}"`, { visible: true });
98
+ if (!venvResult.ok) {
99
+ return { success: false, error: `Failed to create venv: ${venvResult.output}` };
100
+ }
101
+
102
+ // Step 2: Upgrade pip
103
+ console.error('[pii] Upgrading pip...');
104
+ const pipUpgrade = run(`"${VENV_PYTHON}" -m pip install --quiet --upgrade pip`, { visible: true });
105
+ if (!pipUpgrade.ok) {
106
+ return { success: false, error: `Failed to upgrade pip: ${pipUpgrade.output}` };
107
+ }
108
+
109
+ // Step 3: Install Presidio packages
110
+ console.error('[pii] Installing presidio-analyzer, presidio-anonymizer, spacy...');
111
+ const installResult = run(
112
+ `"${VENV_PYTHON}" -m pip install --quiet presidio-analyzer presidio-anonymizer spacy`,
113
+ { visible: true }
114
+ );
115
+ if (!installResult.ok) {
116
+ return { success: false, error: `Failed to install Presidio: ${installResult.output}` };
117
+ }
118
+
119
+ // Step 4: Download spaCy model (~400 MB — used by Presidio's default NLP engine)
120
+ console.error('[pii] Downloading spaCy en_core_web_lg model (~400 MB)...');
121
+ const spaCyResult = run(
122
+ `"${VENV_PYTHON}" -m spacy download en_core_web_lg`,
123
+ { visible: true }
124
+ );
125
+ if (!spaCyResult.ok) {
126
+ return { success: false, error: `Failed to download spaCy model: ${spaCyResult.output}` };
127
+ }
128
+
129
+ // Write marker file
130
+ try {
131
+ writeFileSync(MARKER_FILE, new Date().toISOString());
132
+ } catch (err: unknown) {
133
+ const e = err as { message?: string };
134
+ return { success: false, error: `Failed to write marker file: ${e.message}` };
135
+ }
136
+
137
+ return {
138
+ success: true,
139
+ initialized: true,
140
+ python_path: VENV_PYTHON,
141
+ venv_path: VENV_PATH,
142
+ };
143
+ }
144
+
145
+ // Main
146
+ const result = presidioInit();
147
+ console.log(JSON.stringify(result, null, 2));
148
+
149
+ if (!result.success) {
150
+ process.exit(1);
151
+ }
@@ -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
+ }