my-pi 0.0.11 → 0.0.13

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/dist/api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as create_my_pi, r as runPrintMode, t as InteractiveMode } from "./api-L4-Ei2xx.js";
1
+ import { n as create_my_pi, r as runPrintMode, t as InteractiveMode } from "./api-CWEizv2k.js";
2
2
  export { InteractiveMode, create_my_pi, runPrintMode };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as create_my_pi } from "./api-L4-Ei2xx.js";
2
+ import { n as create_my_pi } from "./api-CWEizv2k.js";
3
3
  import { InteractiveMode, runPrintMode } from "@mariozechner/pi-coding-agent";
4
4
  import { defineCommand, runMain } from "citty";
5
5
  import { readFileSync } from "node:fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-pi",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Composable pi coding agent with MCP, LSP, agent chains, prompt presets, and local eval telemetry",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,7 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- // Extract redact function for direct testing
4
- // (mirrors the logic in filter-output.ts)
5
3
  interface SecretPattern {
6
4
  name: string;
7
5
  pattern: RegExp;
@@ -9,10 +7,11 @@ interface SecretPattern {
9
7
 
10
8
  const SECRET_PATTERNS: SecretPattern[] = [
11
9
  { name: 'AWS Access Key', pattern: /AKIA[A-Z0-9]{16}/g },
10
+ { name: 'AWS Temp Access Key', pattern: /ASIA[A-Z0-9]{16}/g },
12
11
  {
13
12
  name: 'AWS Secret Key',
14
13
  pattern:
15
- /(?:SecretAccessKey|aws_secret_access_key)\s*[:=]\s*[A-Za-z0-9/+=]{40}/g,
14
+ /\b(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key|secret_access_key|SecretAccessKey)\b\s*[:=]\s*["']?[A-Za-z0-9/+=]{40,}["']?/g,
16
15
  },
17
16
  {
18
17
  name: 'Bearer Token',
@@ -30,18 +29,42 @@ const SECRET_PATTERNS: SecretPattern[] = [
30
29
  name: 'Stripe Test Key',
31
30
  pattern: /sk_test_[a-zA-Z0-9]{20,}/g,
32
31
  },
32
+ {
33
+ name: 'Hetzner Token',
34
+ pattern:
35
+ /(?:HCLOUD_TOKEN|hcloud_token|token)\s*[:=]\s*["']?[a-f0-9]{64}\b/g,
36
+ },
33
37
  {
34
38
  name: 'Private Key',
35
- pattern: /-----BEGIN\s+[\w\s]*PRIVATE\s+KEY-----/g,
39
+ pattern:
40
+ /-----BEGIN\s+[\w\s]*PRIVATE\s+KEY-----[\s\S]*?-----END\s+[\w\s]*PRIVATE\s+KEY-----/g,
36
41
  },
37
42
  {
38
43
  name: 'Connection String with Password',
39
44
  pattern: /:\/\/[^:]+:[^@]+@/g,
40
45
  },
46
+ {
47
+ name: 'Generic Password Field',
48
+ pattern:
49
+ /\b[\w-]*(?:password|passwd|secret|token|api[_-]?key)\b\s*[:=]\s*["']?[A-Za-z0-9._:/+=@!-]{8,}/gi,
50
+ },
51
+ {
52
+ name: 'Generic Secret Phrase',
53
+ pattern:
54
+ /\b(?:password|passwd|secret|token|api[_-]?key)\b(?:\s+(?:is|was|seen|value|header))?\s*[:=]?\s+[A-Za-z0-9._:/+=@!-]{8,}/gi,
55
+ },
41
56
  {
42
57
  name: 'Tavily API Key',
43
58
  pattern: /tvly-[a-zA-Z0-9_-]{20,}/g,
44
59
  },
60
+ {
61
+ name: 'Kagi API Key',
62
+ pattern: /[a-zA-Z0-9_-]{40,}\.[a-zA-Z0-9_-]{40,}/g,
63
+ },
64
+ {
65
+ name: 'Brave API Key',
66
+ pattern: /BSA[A-Z0-9]{20,}/g,
67
+ },
45
68
  {
46
69
  name: 'Firecrawl API Key',
47
70
  pattern: /fc-[a-f0-9]{32}/g,
@@ -50,6 +73,10 @@ const SECRET_PATTERNS: SecretPattern[] = [
50
73
  name: 'GitHub Token',
51
74
  pattern: /gh[pousr]_[a-zA-Z0-9]{36,}/g,
52
75
  },
76
+ {
77
+ name: 'GitHub Fine-grained PAT',
78
+ pattern: /github_pat_[a-zA-Z0-9_]{20,}/g,
79
+ },
53
80
  ];
54
81
 
55
82
  function redact(text: string): { redacted: string; count: number } {
@@ -70,16 +97,46 @@ function redact(text: string): { redacted: string; count: number } {
70
97
 
71
98
  describe('redact', () => {
72
99
  it('redacts AWS access keys', () => {
73
- const input = 'key: AKIAIOSFODNN7EXAMPLE';
100
+ const input = 'key: AKIA1234567890CANARY';
74
101
  const { redacted, count } = redact(input);
75
102
  expect(count).toBe(1);
76
103
  expect(redacted).toContain('[REDACTED:AWS Access Key]');
77
- expect(redacted).not.toContain('AKIAIOSFODNN7EXAMPLE');
104
+ expect(redacted).not.toContain('AKIA1234567890CANARY');
105
+ });
106
+
107
+ it('redacts AWS temp access keys', () => {
108
+ const input = 'key: ASIA1234567890CANARY';
109
+ const { redacted, count } = redact(input);
110
+ expect(count).toBe(1);
111
+ expect(redacted).toContain('[REDACTED:AWS Temp Access Key]');
112
+ expect(redacted).not.toContain('ASIA1234567890CANARY');
113
+ });
114
+
115
+ it('redacts uppercase AWS secret env vars', () => {
116
+ const input =
117
+ 'AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG+bPxRfiCYCANARYKEY01';
118
+ const { redacted, count } = redact(input);
119
+ expect(count).toBe(1);
120
+ expect(redacted).toContain('[REDACTED:AWS Secret Key]');
121
+ expect(redacted).not.toContain(
122
+ 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYCANARYKEY01',
123
+ );
124
+ });
125
+
126
+ it('redacts lower-case secret_access_key assignments', () => {
127
+ const input =
128
+ 'secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYCANARYKEY01"';
129
+ const { redacted, count } = redact(input);
130
+ expect(count).toBe(1);
131
+ expect(redacted).toContain('[REDACTED:AWS Secret Key]');
132
+ expect(redacted).not.toContain(
133
+ 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYCANARYKEY01',
134
+ );
78
135
  });
79
136
 
80
137
  it('redacts bearer tokens', () => {
81
138
  const input =
82
- 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.abc123';
139
+ 'Authorization: Bearer canaryBearerTokenValueAlphaNum1234567890ZZ';
83
140
  const { redacted, count } = redact(input);
84
141
  expect(count).toBe(1);
85
142
  expect(redacted).toContain('[REDACTED:Bearer Token]');
@@ -87,10 +144,13 @@ describe('redact', () => {
87
144
 
88
145
  it('redacts OpenAI/Anthropic API keys', () => {
89
146
  const input =
90
- 'ANTHROPIC_API_KEY=sk-ant-api03-abcdefghijklmnopqrstuvwxyz';
147
+ 'ANTHROPIC_API_KEY=sk-ant-api-key-example-123456789012345';
91
148
  const { redacted, count } = redact(input);
92
149
  expect(count).toBe(1);
93
150
  expect(redacted).toContain('[REDACTED:OpenAI/Anthropic API Key]');
151
+ expect(redacted).not.toContain(
152
+ '[REDACTED:Generic Password Field]',
153
+ );
94
154
  });
95
155
 
96
156
  it('redacts Stripe live keys', () => {
@@ -100,15 +160,26 @@ describe('redact', () => {
100
160
  expect(redacted).toContain('[REDACTED:Stripe Live Key]');
101
161
  });
102
162
 
103
- it('redacts private key headers', () => {
104
- const input = '-----BEGIN RSA PRIVATE KEY-----';
163
+ it('redacts Hetzner tokens', () => {
164
+ const input = `HCLOUD_TOKEN=${'a'.repeat(64)}`;
165
+ const { redacted, count } = redact(input);
166
+ expect(count).toBe(1);
167
+ expect(redacted).toContain('[REDACTED:Hetzner Token]');
168
+ expect(redacted).not.toContain('a'.repeat(64));
169
+ });
170
+
171
+ it('redacts full private key blocks', () => {
172
+ const input = `-----BEGIN PRIVATE KEY-----\nQ0FOQVJZX1BSSVZBVEVfS0VZX0JMT0NLX0xJTkVfMDAx\nQ0FOQVJZX1BSSVZBVEVfS0VZX0JMT0NLX0xJTkVfMDAy\n-----END PRIVATE KEY-----`;
105
173
  const { redacted, count } = redact(input);
106
174
  expect(count).toBe(1);
107
175
  expect(redacted).toContain('[REDACTED:Private Key]');
176
+ expect(redacted).not.toContain(
177
+ 'Q0FOQVJZX1BSSVZBVEVfS0VZX0JMT0NLX0xJTkVfMDAx',
178
+ );
108
179
  });
109
180
 
110
181
  it('redacts connection strings with passwords', () => {
111
- const input = 'postgres://user:s3cretP@ss@localhost:5432/db';
182
+ const input = 'postgres://user:supersecretpass@localhost:5432/db';
112
183
  const { redacted, count } = redact(input);
113
184
  expect(count).toBe(1);
114
185
  expect(redacted).toContain(
@@ -116,34 +187,78 @@ describe('redact', () => {
116
187
  );
117
188
  });
118
189
 
119
- it('redacts Tavily API keys', () => {
190
+ it('redacts prefixed generic secret fields', () => {
191
+ const input =
192
+ 'OPAQUE_SECRET=cnyr_ZmFrZVNlY3JldFZhbHVlX1JlZGFjdGlvbl9TdWl0ZV8wMDE';
193
+ const { redacted, count } = redact(input);
194
+ expect(count).toBe(1);
195
+ expect(redacted).toContain('[REDACTED:Generic Password Field]');
196
+ expect(redacted).not.toContain(
197
+ 'cnyr_ZmFrZVNlY3JldFZhbHVlX1JlZGFjdGlvbl9TdWl0ZV8wMDE',
198
+ );
199
+ });
200
+
201
+ it('redacts freeform secret phrases in logs', () => {
120
202
  const input =
121
- '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';
203
+ '2026-04-19T09:00:02Z INFO opaque fallback secret cnyr_ZmFrZVNlY3JldFZhbHVlX1JlZGFjdGlvbl9TdWl0ZV8wMDE';
204
+ const { redacted, count } = redact(input);
205
+ expect(count).toBe(1);
206
+ expect(redacted).toContain('[REDACTED:Generic Secret Phrase]');
207
+ expect(redacted).not.toContain(
208
+ 'cnyr_ZmFrZVNlY3JldFZhbHVlX1JlZGFjdGlvbl9TdWl0ZV8wMDE',
209
+ );
210
+ });
211
+
212
+ it('redacts Tavily API keys', () => {
213
+ const input = 'tvly-canary-redaction-suite-000000000000000001';
122
214
  const { redacted, count } = redact(input);
123
215
  expect(count).toBe(1);
124
216
  expect(redacted).toContain('[REDACTED:Tavily API Key]');
125
217
  });
126
218
 
219
+ it('redacts Kagi API keys', () => {
220
+ const input = `${'a'.repeat(40)}.${'b'.repeat(40)}`;
221
+ const { redacted, count } = redact(input);
222
+ expect(count).toBe(1);
223
+ expect(redacted).toContain('[REDACTED:Kagi API Key]');
224
+ });
225
+
226
+ it('redacts Brave API keys', () => {
227
+ const input = 'BSA' + 'A'.repeat(20);
228
+ const { redacted, count } = redact(input);
229
+ expect(count).toBe(1);
230
+ expect(redacted).toContain('[REDACTED:Brave API Key]');
231
+ });
232
+
127
233
  it('redacts Firecrawl API keys', () => {
128
- const input = 'fc-e3011a33574e44c8aa539c24218cd659';
234
+ const input = 'fc-e3b0c44298fc1c149afbf4c8996fb924';
129
235
  const { redacted, count } = redact(input);
130
236
  expect(count).toBe(1);
131
237
  expect(redacted).toContain('[REDACTED:Firecrawl API Key]');
132
238
  });
133
239
 
134
240
  it('redacts GitHub tokens', () => {
135
- const input = 'ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn';
241
+ const input =
242
+ 'ghp_CanaryRedactionSuite000000000000000000000001ABCD';
136
243
  const { redacted, count } = redact(input);
137
244
  expect(count).toBe(1);
138
245
  expect(redacted).toContain('[REDACTED:GitHub Token]');
139
246
  });
140
247
 
248
+ it('redacts GitHub fine-grained PATs', () => {
249
+ const input = 'github_pat_' + 'A'.repeat(30);
250
+ const { redacted, count } = redact(input);
251
+ expect(count).toBe(1);
252
+ expect(redacted).toContain('[REDACTED:GitHub Fine-grained PAT]');
253
+ });
254
+
141
255
  it('redacts multiple secrets in one string', () => {
142
256
  const input =
143
- 'aws: AKIAIOSFODNN7EXAMPLE, token: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.longtoken';
257
+ 'aws: AKIA1234567890CANARY, SERVICE_PASSWORD=CanaryPassword-Redaction-001!';
144
258
  const { redacted, count } = redact(input);
145
259
  expect(count).toBe(2);
146
- expect(redacted).not.toContain('AKIAIOSFODNN7EXAMPLE');
260
+ expect(redacted).not.toContain('AKIA1234567890CANARY');
261
+ expect(redacted).not.toContain('CanaryPassword-Redaction-001!');
147
262
  });
148
263
 
149
264
  it('leaves clean text unchanged', () => {
@@ -154,7 +269,7 @@ describe('redact', () => {
154
269
  });
155
270
 
156
271
  it('preserves prefix in redacted output', () => {
157
- const input = 'AKIAIOSFODNN7EXAMPLE';
272
+ const input = 'AKIA1234567890CANARY';
158
273
  const { redacted } = redact(input);
159
274
  expect(redacted).toMatch(/^AKIA/);
160
275
  });
@@ -10,10 +10,11 @@ interface SecretPattern {
10
10
 
11
11
  const SECRET_PATTERNS: SecretPattern[] = [
12
12
  { name: 'AWS Access Key', pattern: /AKIA[A-Z0-9]{16}/g },
13
+ { name: 'AWS Temp Access Key', pattern: /ASIA[A-Z0-9]{16}/g },
13
14
  {
14
15
  name: 'AWS Secret Key',
15
16
  pattern:
16
- /(?:SecretAccessKey|aws_secret_access_key)\s*[:=]\s*[A-Za-z0-9/+=]{40}/g,
17
+ /\b(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key|secret_access_key|SecretAccessKey)\b\s*[:=]\s*["']?[A-Za-z0-9/+=]{40,}["']?/g,
17
18
  },
18
19
  {
19
20
  name: 'Bearer Token',
@@ -38,7 +39,8 @@ const SECRET_PATTERNS: SecretPattern[] = [
38
39
  },
39
40
  {
40
41
  name: 'Private Key',
41
- pattern: /-----BEGIN\s+[\w\s]*PRIVATE\s+KEY-----/g,
42
+ pattern:
43
+ /-----BEGIN\s+[\w\s]*PRIVATE\s+KEY-----[\s\S]*?-----END\s+[\w\s]*PRIVATE\s+KEY-----/g,
42
44
  },
43
45
  {
44
46
  name: 'Connection String with Password',
@@ -47,7 +49,12 @@ const SECRET_PATTERNS: SecretPattern[] = [
47
49
  {
48
50
  name: 'Generic Password Field',
49
51
  pattern:
50
- /(?:password|passwd|secret|token)\s*[:=]\s*["']?[^\s"']{8,}/gi,
52
+ /\b[\w-]*(?:password|passwd|secret|token|api[_-]?key)\b\s*[:=]\s*["']?[A-Za-z0-9._:/+=@!-]{8,}/gi,
53
+ },
54
+ {
55
+ name: 'Generic Secret Phrase',
56
+ pattern:
57
+ /\b(?:password|passwd|secret|token|api[_-]?key)\b(?:\s+(?:is|was|seen|value|header))?\s*[:=]?\s+[A-Za-z0-9._:/+=@!-]{8,}/gi,
51
58
  },
52
59
  {
53
60
  name: 'Tavily API Key',
@@ -69,6 +76,10 @@ const SECRET_PATTERNS: SecretPattern[] = [
69
76
  name: 'GitHub Token',
70
77
  pattern: /gh[pousr]_[a-zA-Z0-9]{36,}/g,
71
78
  },
79
+ {
80
+ name: 'GitHub Fine-grained PAT',
81
+ pattern: /github_pat_[a-zA-Z0-9_]{20,}/g,
82
+ },
72
83
  ];
73
84
 
74
85
  function redact(text: string): { redacted: string; count: number } {
@@ -76,7 +87,6 @@ function redact(text: string): { redacted: string; count: number } {
76
87
  let result = text;
77
88
 
78
89
  for (const sp of SECRET_PATTERNS) {
79
- // Reset lastIndex for global regexes
80
90
  sp.pattern.lastIndex = 0;
81
91
  result = result.replace(sp.pattern, (match) => {
82
92
  count++;
@@ -88,11 +98,9 @@ function redact(text: string): { redacted: string; count: number } {
88
98
  return { redacted: result, count };
89
99
  }
90
100
 
91
- // Default export for Pi Package / additionalExtensionPaths loading
92
101
  export default async function filter_output(pi: ExtensionAPI) {
93
102
  let totalRedacted = 0;
94
103
 
95
- // Intercept tool results to redact secrets before the LLM sees them
96
104
  pi.on('tool_result' as const, async (event: any) => {
97
105
  if (!event.content) return;
98
106
 
@@ -5,7 +5,7 @@ import {
5
5
  format_telemetry_status,
6
6
  infer_run_outcome,
7
7
  parse_telemetry_command,
8
- } from './otel.js';
8
+ } from './telemetry.js';
9
9
 
10
10
  describe('format_telemetry_status', () => {
11
11
  it('includes saved state, effective state, override, and db path', () => {