@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,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
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "droid-project",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Manage project context files for persistent AI memory across sessions. Load, update, or create project context before working on multi-session features.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Orderful",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name: project
|
|
2
2
|
description: "Manage project context files for persistent AI memory across sessions. Load, update, or create project context before working on multi-session features."
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.4.0
|
|
4
4
|
status: beta
|
|
5
5
|
|
|
6
6
|
includes:
|
|
@@ -21,7 +21,7 @@ config_schema:
|
|
|
21
21
|
required: true
|
|
22
22
|
preset:
|
|
23
23
|
type: select
|
|
24
|
-
description:
|
|
24
|
+
description: "Link style: obsidian uses [[wikilinks]], markdown uses [standard](links)"
|
|
25
25
|
default: "markdown"
|
|
26
26
|
options:
|
|
27
27
|
- markdown
|
|
@@ -7,7 +7,7 @@ allowed-tools: [Read, Write, Glob, Grep, Bash]
|
|
|
7
7
|
|
|
8
8
|
# Project Skill
|
|
9
9
|
|
|
10
|
-
Persistent project context that survives across sessions. Projects are folders containing `PROJECT.md` + `CHANGELOG.md
|
|
10
|
+
Persistent project context that survives across sessions. Projects are folders containing `PROJECT.md` + `CHANGELOG.md`, with optional companion docs (`DECISIONS.md`, `WORKLOG.md`) that emerge as the project grows.
|
|
11
11
|
|
|
12
12
|
## Why Projects?
|
|
13
13
|
|
|
@@ -31,7 +31,7 @@ Chat history disappears. Projects persist.
|
|
|
31
31
|
| Setting | Default | Description |
|
|
32
32
|
| -------------- | ------------ | -------------------------------------------------- |
|
|
33
33
|
| `projects_dir` | **required** | Where projects are stored (must be configured) |
|
|
34
|
-
| `preset` | `markdown` |
|
|
34
|
+
| `preset` | `markdown` | Link style: `obsidian` uses `[[wikilinks]]`, `markdown` uses `[standard](links)` |
|
|
35
35
|
| `override` | (none) | User-defined behaviour overrides |
|
|
36
36
|
|
|
37
37
|
**Overrides:** This skill supports user-defined overrides. See `/droid` skill § Skill Overrides.
|
|
@@ -106,6 +106,7 @@ Projects work best in a cycle:
|
|
|
106
106
|
3. **Document** - Capture decisions in PROJECT.md before implementing
|
|
107
107
|
4. **Implement** - Build with full context of why decisions were made
|
|
108
108
|
5. **Capture** - `/project update` saves new learnings
|
|
109
|
-
6. **
|
|
109
|
+
6. **Prune** - If PROJECT.md exceeds ~300 lines, archive completed work to WORKLOG.md
|
|
110
|
+
7. **Repeat** - Next session starts with accumulated knowledge
|
|
110
111
|
|
|
111
112
|
Each cycle starts further ahead than the last.
|
|
@@ -5,7 +5,7 @@ Projects use a split file pattern to minimize context usage when loading.
|
|
|
5
5
|
## File Structure
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG.md` (sibling to PROJECT.md) - Complete version history
|
|
8
|
-
- `PROJECT.md` -
|
|
8
|
+
- `PROJECT.md` - Link to full history only (version number lives in the metadata table)
|
|
9
9
|
|
|
10
10
|
## On Update
|
|
11
11
|
|
|
@@ -15,8 +15,8 @@ Projects use a split file pattern to minimize context usage when loading.
|
|
|
15
15
|
2. **Add new entry to CHANGELOG.md** (prepend after header)
|
|
16
16
|
|
|
17
17
|
3. **Update PROJECT.md changelog section**
|
|
18
|
-
- Keep only
|
|
19
|
-
-
|
|
18
|
+
- Keep only a link to CHANGELOG.md — no inline entries
|
|
19
|
+
- The version number in the metadata table is the quick-glance indicator
|
|
20
20
|
|
|
21
21
|
## CHANGELOG.md Format
|
|
22
22
|
|
|
@@ -35,31 +35,20 @@ All notable changes to the {Project Name} project.
|
|
|
35
35
|
|
|
36
36
|
## PROJECT.md Changelog Section Format
|
|
37
37
|
|
|
38
|
-
**Markdown preset:**
|
|
39
38
|
```markdown
|
|
40
39
|
## Changelog
|
|
41
40
|
|
|
42
41
|
See [CHANGELOG.md](CHANGELOG.md) for full history.
|
|
43
|
-
|
|
44
|
-
### X.Y.Z - YYYY-MM-DD
|
|
45
|
-
- [Most recent]
|
|
46
|
-
|
|
47
|
-
### X.Y.Z - YYYY-MM-DD
|
|
48
|
-
- [Second most recent]
|
|
49
|
-
|
|
50
|
-
### X.Y.Z - YYYY-MM-DD
|
|
51
|
-
- [Third most recent]
|
|
52
42
|
```
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
```markdown
|
|
56
|
-
## Changelog
|
|
44
|
+
When `preset` is `obsidian`, use `See [[CHANGELOG|full history]].` instead.
|
|
57
45
|
|
|
58
|
-
|
|
46
|
+
## Migration
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
If an existing PROJECT.md has inline changelog entries:
|
|
49
|
+
1. Ensure all entries exist in CHANGELOG.md
|
|
50
|
+
2. Replace the inline entries with just the link
|
|
51
|
+
3. This happens naturally during `/project update` — no need to proactively migrate
|
|
63
52
|
|
|
64
53
|
## Guidelines
|
|
65
54
|
|
|
@@ -67,4 +56,4 @@ See [[CHANGELOG|full history]].
|
|
|
67
56
|
- Keep entries concise but descriptive
|
|
68
57
|
- Group related changes in a single bullet when appropriate
|
|
69
58
|
- Most recent version at the top
|
|
70
|
-
- Use `##` headers in CHANGELOG.md
|
|
59
|
+
- Use `##` headers in CHANGELOG.md
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
1. **Read config first**
|
|
8
8
|
- Run `droid config --get tools.project` and parse the JSON output
|
|
9
9
|
- Use `projects_dir` (required - if not configured, inform user to set it up)
|
|
10
|
-
- Use `preset` if configured (markdown or obsidian)
|
|
11
10
|
|
|
12
11
|
2. **Get project name**
|
|
13
12
|
- Use provided name, or ask if not provided
|
|
@@ -21,7 +20,6 @@
|
|
|
21
20
|
4. **Create files from templates** (see `templates.md`)
|
|
22
21
|
- `PROJECT.md` - Main context file
|
|
23
22
|
- `CHANGELOG.md` - Version history
|
|
24
|
-
- Format varies by `preset` config (markdown vs obsidian)
|
|
25
23
|
|
|
26
24
|
5. **Confirm creation**
|
|
27
25
|
- Show created folder path
|
|
@@ -62,6 +60,18 @@ New projects start minimal and grow organically. Common sections added over time
|
|
|
62
60
|
|
|
63
61
|
See `templates.md` for the full initial template.
|
|
64
62
|
|
|
63
|
+
### Companion Docs
|
|
64
|
+
|
|
65
|
+
Projects may grow companion docs over time. These are **never** created at project creation — they emerge as the project grows.
|
|
66
|
+
|
|
67
|
+
| Doc | Created When | Purpose |
|
|
68
|
+
|-----|-------------|---------|
|
|
69
|
+
| `CHANGELOG.md` | At project creation (the one exception) | Version history |
|
|
70
|
+
| `DECISIONS.md` | 3+ decisions accumulate in PROJECT.md | Dedicated decisions log |
|
|
71
|
+
| `WORKLOG.md` | PROJECT.md exceeds ~300 lines with completed items | Archive of completed work |
|
|
72
|
+
|
|
73
|
+
The `/project update` procedure handles creation and migration automatically — see `updating.md` for details.
|
|
74
|
+
|
|
65
75
|
---
|
|
66
76
|
|
|
67
77
|
## Creating from Codex
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
- Confirm which project was loaded
|
|
33
33
|
- Summarize key context (2-3 sentences)
|
|
34
34
|
- Use project contents for all subsequent work in the session
|
|
35
|
-
- **Do NOT read CHANGELOG.md
|
|
35
|
+
- **Do NOT read companion docs by default** — CHANGELOG.md, DECISIONS.md, and WORKLOG.md exist for on-demand lookup only. PROJECT.md already references them; that is enough context.
|
|
36
|
+
- If the user asks about decisions, past work, or history, THEN read the relevant companion doc.
|
|
36
37
|
|
|
37
38
|
7. **If `-- {instruction}` provided:** Execute the follow-up instruction against the loaded project
|
|
38
39
|
|