greprag 5.38.0 → 5.40.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.
@@ -0,0 +1,368 @@
1
+ "use strict";
2
+ // adr: adr/secret-scrubber.md
3
+ /** Secret scrubber — pure, FILESYSTEM-FREE secret detection.
4
+ *
5
+ * MIRROR of packages/core/src/secret-scrubber.ts (the canonical source). The
6
+ * CLI ships as a zero-dependency `tsc` artifact — the Stop hook cannot import
7
+ * @greprag/core — so the detection logic is duplicated here. KEEP IN SYNC with
8
+ * core; everything below the header is byte-identical, and the sync guard in
9
+ * tests/test-secret-scrubber.cjs fails the build on drift.
10
+ *
11
+ * Only the pure detection (SECRET_PATTERNS / scrubString / scrubObject /
12
+ * redactHighEntropy) is mirrored. The .env / process.env walker
13
+ * (`buildRedactionMap`) is client-only and stays in hook.ts, not here.
14
+ *
15
+ * Patterns adapted from gitleaks (https://github.com/gitleaks/gitleaks, MIT
16
+ * License) and published provider key formats. Detection is best-effort
17
+ * defense-in-depth — never a guarantee. Output: every redaction replaces the
18
+ * secret with `[REDACTED:TYPE]`. Replacement order inside scrubString:
19
+ * (1) caller value-map (longest-first); (2) provider patterns; (3) entropy.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.SECRET_PATTERNS = void 0;
23
+ exports.shannonEntropy = shannonEntropy;
24
+ exports.isHighEntropyToken = isHighEntropyToken;
25
+ exports.redactHighEntropy = redactHighEntropy;
26
+ exports.scrubString = scrubString;
27
+ exports.scrubObject = scrubObject;
28
+ /** Provider secret patterns.
29
+ *
30
+ * The FIRST 18 entries are the original hook ruleset, preserved VERBATIM and
31
+ * in their original order so existing behavior is byte-identical — DO NOT
32
+ * reorder, edit, or relabel them. Enriched families are appended after; because
33
+ * String.replace runs sequentially, an earlier (original) pattern always wins
34
+ * for inputs it already matched, so the original-18 outputs never change. */
35
+ exports.SECRET_PATTERNS = [
36
+ // ─── ORIGINAL 18 — behavior-preserving, verbatim from the hook ────────────
37
+ // Anthropic — sk-ant-* (must match before generic sk-*)
38
+ [/\bsk-ant-[A-Za-z0-9_-]{30,}/g, '[REDACTED:ANTHROPIC_KEY]'],
39
+ // OpenAI — sk-* (project keys are sk-proj-*, also covered)
40
+ [/\bsk-[A-Za-z0-9_-]{30,}/g, '[REDACTED:OPENAI_KEY]'],
41
+ // Stripe
42
+ [/\bsk_live_[A-Za-z0-9]{20,}/g, '[REDACTED:STRIPE_LIVE]'],
43
+ [/\bsk_test_[A-Za-z0-9]{20,}/g, '[REDACTED:STRIPE_TEST]'],
44
+ [/\brk_live_[A-Za-z0-9]{20,}/g, '[REDACTED:STRIPE_RESTRICTED]'],
45
+ [/\bwhsec_[A-Za-z0-9]{20,}/g, '[REDACTED:STRIPE_WEBHOOK]'],
46
+ // GitHub
47
+ [/\bghp_[A-Za-z0-9]{30,}/g, '[REDACTED:GITHUB_PAT]'],
48
+ [/\bgho_[A-Za-z0-9]{30,}/g, '[REDACTED:GITHUB_OAUTH]'],
49
+ [/\bghu_[A-Za-z0-9]{30,}/g, '[REDACTED:GITHUB_USER]'],
50
+ [/\bghs_[A-Za-z0-9]{30,}/g, '[REDACTED:GITHUB_SERVER]'],
51
+ [/\bghr_[A-Za-z0-9]{30,}/g, '[REDACTED:GITHUB_REFRESH]'],
52
+ // AWS
53
+ [/\bAKIA[A-Z0-9]{16}\b/g, '[REDACTED:AWS_ACCESS_KEY]'],
54
+ [/\bASIA[A-Z0-9]{16}\b/g, '[REDACTED:AWS_SESSION_KEY]'],
55
+ // Google — API keys vary 35–50 chars after AIza. No trailing \b: hyphens
56
+ // and dashes inside the key break the word boundary.
57
+ [/\bAIza[A-Za-z0-9_-]{30,50}/g, '[REDACTED:GOOGLE_API_KEY]'],
58
+ [/\bya29\.[A-Za-z0-9_-]{20,}/g, '[REDACTED:GOOGLE_OAUTH]'],
59
+ // Slack
60
+ [/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, '[REDACTED:SLACK_TOKEN]'],
61
+ // JWTs (eyJ…eyJ…sig) — common for session tokens, Auth0, Firebase
62
+ [/\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[REDACTED:JWT]'],
63
+ // Bearer tokens in Authorization headers (catches ad-hoc curl examples)
64
+ [/Authorization:\s*Bearer\s+[A-Za-z0-9_.-]{20,}/gi, 'Authorization: Bearer [REDACTED]'],
65
+ // ─── ENRICHED FAMILIES (gitleaks-derived; appended, never reordering above) ─
66
+ // ── Anthropic / OpenAI siblings ──
67
+ [/\bsk-ant-admin01-[A-Za-z0-9_-]{30,}/g, '[REDACTED:ANTHROPIC_ADMIN_KEY]'],
68
+ // ── GitHub fine-grained + Actions/refresh ──
69
+ [/\bgithub_pat_[A-Za-z0-9_]{22,}/g, '[REDACTED:GITHUB_FINE_GRAINED_PAT]'],
70
+ // ── GitLab ──
71
+ [/\bglpat-[A-Za-z0-9_-]{20,}/g, '[REDACTED:GITLAB_PAT]'],
72
+ [/\bgldt-[A-Za-z0-9_-]{20,}/g, '[REDACTED:GITLAB_DEPLOY_TOKEN]'],
73
+ [/\bglrt-[A-Za-z0-9_-]{20,}/g, '[REDACTED:GITLAB_RUNNER_TOKEN]'],
74
+ [/\bglptt-[A-Za-z0-9_-]{20,}/g, '[REDACTED:GITLAB_PIPELINE_TRIGGER]'],
75
+ [/\bgloas-[A-Za-z0-9_-]{20,}/g, '[REDACTED:GITLAB_OAUTH]'],
76
+ [/\bglcbt-[A-Za-z0-9_-]{20,}/g, '[REDACTED:GITLAB_CICD_JOB_TOKEN]'],
77
+ // ── Atlassian / Bitbucket ──
78
+ [/\bATATT3[A-Za-z0-9_=-]{20,}/g, '[REDACTED:ATLASSIAN_API_TOKEN]'],
79
+ [/\bATCTT3[A-Za-z0-9_=-]{20,}/g, '[REDACTED:ATLASSIAN_CONNECT_TOKEN]'],
80
+ [/\bATBB[A-Za-z0-9]{30,}/g, '[REDACTED:BITBUCKET_TOKEN]'],
81
+ // ── Stripe extras ──
82
+ [/\brk_test_[A-Za-z0-9]{20,}/g, '[REDACTED:STRIPE_RESTRICTED_TEST]'],
83
+ // ── Square ──
84
+ [/\bsq0atp-[A-Za-z0-9_-]{22}/g, '[REDACTED:SQUARE_ACCESS_TOKEN]'],
85
+ [/\bsq0csp-[A-Za-z0-9_-]{43}/g, '[REDACTED:SQUARE_OAUTH_SECRET]'],
86
+ [/\bEAAA[A-Za-z0-9_-]{60}/g, '[REDACTED:SQUARE_PRODUCTION_TOKEN]'],
87
+ // ── PayPal / Braintree ──
88
+ [/\baccess_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}/g, '[REDACTED:PAYPAL_BRAINTREE_TOKEN]'],
89
+ // ── Slack webhook + app/config tokens ──
90
+ [/https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_]+\/B[A-Za-z0-9_]+\/[A-Za-z0-9_]+/g, '[REDACTED:SLACK_WEBHOOK]'],
91
+ [/\bxapp-[0-9]-[A-Za-z0-9-]{10,}/g, '[REDACTED:SLACK_APP_TOKEN]'],
92
+ [/\bxoxe\.xox[bp]-[0-9]-[A-Za-z0-9-]{10,}/g, '[REDACTED:SLACK_CONFIG_TOKEN]'],
93
+ // ── Twilio ──
94
+ [/\bSK[0-9a-fA-F]{32}\b/g, '[REDACTED:TWILIO_API_KEY]'],
95
+ [/\bAC[0-9a-fA-F]{32}\b/g, '[REDACTED:TWILIO_ACCOUNT_SID]'],
96
+ // ── SendGrid / Mailgun / Mailchimp / Sendinblue ──
97
+ [/\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, '[REDACTED:SENDGRID_KEY]'],
98
+ [/\bkey-[0-9a-zA-Z]{32}\b/g, '[REDACTED:MAILGUN_KEY]'],
99
+ [/\b[0-9a-f]{32}-us[0-9]{1,2}\b/g, '[REDACTED:MAILCHIMP_KEY]'],
100
+ [/\bxkeysib-[a-f0-9]{64}-[A-Za-z0-9]{16}/g, '[REDACTED:SENDINBLUE_KEY]'],
101
+ // ── Discord ──
102
+ [/\b[MNO][A-Za-z0-9_-]{23,25}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}/g, '[REDACTED:DISCORD_BOT_TOKEN]'],
103
+ [/https:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g, '[REDACTED:DISCORD_WEBHOOK]'],
104
+ // ── AI / ML providers ──
105
+ [/\bhf_[A-Za-z0-9]{34,}/g, '[REDACTED:HUGGINGFACE_KEY]'],
106
+ [/\br8_[A-Za-z0-9]{37,}/g, '[REDACTED:REPLICATE_KEY]'],
107
+ // ── Package registries ──
108
+ [/\bnpm_[A-Za-z0-9]{36}\b/g, '[REDACTED:NPM_TOKEN]'],
109
+ [/\bpypi-AgEIcHlwaS[A-Za-z0-9_-]{50,}/g, '[REDACTED:PYPI_TOKEN]'],
110
+ [/\brubygems_[a-f0-9]{48}\b/g, '[REDACTED:RUBYGEMS_KEY]'],
111
+ [/\bdckr_pat_[A-Za-z0-9_-]{27,}/g, '[REDACTED:DOCKERHUB_PAT]'],
112
+ [/\bCLOJARS_[a-z0-9]{60}/g, '[REDACTED:CLOJARS_TOKEN]'],
113
+ // ── Google OAuth client secret + Firebase Cloud Messaging ──
114
+ [/\bGOCSPX-[A-Za-z0-9_-]{28,}/g, '[REDACTED:GCP_OAUTH_SECRET]'],
115
+ [/\bAAAA[A-Za-z0-9_-]{7}:APA91b[A-Za-z0-9_-]{130,}/g, '[REDACTED:FCM_SERVER_KEY]'],
116
+ // ── New Relic ──
117
+ [/\bNRAK-[A-Z0-9]{27}\b/g, '[REDACTED:NEWRELIC_USER_KEY]'],
118
+ [/\bNRJS-[a-f0-9]{19}\b/g, '[REDACTED:NEWRELIC_BROWSER_KEY]'],
119
+ [/\bNRAA-[a-f0-9]{27}\b/g, '[REDACTED:NEWRELIC_ADMIN_KEY]'],
120
+ [/\bNRII-[A-Za-z0-9_-]{32}\b/g, '[REDACTED:NEWRELIC_INSIGHTS_KEY]'],
121
+ // ── Shopify ──
122
+ [/\bshpat_[a-fA-F0-9]{32}\b/g, '[REDACTED:SHOPIFY_ACCESS_TOKEN]'],
123
+ [/\bshpss_[a-fA-F0-9]{32}\b/g, '[REDACTED:SHOPIFY_SHARED_SECRET]'],
124
+ [/\bshpca_[a-fA-F0-9]{32}\b/g, '[REDACTED:SHOPIFY_CUSTOM_APP]'],
125
+ [/\bshppa_[a-fA-F0-9]{32}\b/g, '[REDACTED:SHOPIFY_PRIVATE_APP]'],
126
+ // ── Linear / Notion / Airtable / Contentful / Typeform ──
127
+ [/\blin_api_[A-Za-z0-9]{40,}/g, '[REDACTED:LINEAR_API_KEY]'],
128
+ [/\blin_oauth_[A-Za-z0-9]{40,}/g, '[REDACTED:LINEAR_OAUTH]'],
129
+ [/\bsecret_[A-Za-z0-9]{43}\b/g, '[REDACTED:NOTION_TOKEN]'],
130
+ [/\bntn_[A-Za-z0-9]{46,}/g, '[REDACTED:NOTION_TOKEN]'],
131
+ [/\bpat[A-Za-z0-9]{14}\.[a-f0-9]{64}\b/g, '[REDACTED:AIRTABLE_PAT]'],
132
+ [/\bCFPAT-[A-Za-z0-9_-]{43}/g, '[REDACTED:CONTENTFUL_PAT]'],
133
+ [/\btfp_[A-Za-z0-9._=-]{59}/g, '[REDACTED:TYPEFORM_TOKEN]'],
134
+ // ── DigitalOcean ──
135
+ [/\bdop_v1_[a-f0-9]{64}\b/g, '[REDACTED:DIGITALOCEAN_PAT]'],
136
+ [/\bdoo_v1_[a-f0-9]{64}\b/g, '[REDACTED:DIGITALOCEAN_OAUTH]'],
137
+ [/\bdor_v1_[a-f0-9]{64}\b/g, '[REDACTED:DIGITALOCEAN_REFRESH]'],
138
+ // ── Grafana / Sentry ──
139
+ [/\bglc_[A-Za-z0-9+/]{32,}={0,2}/g, '[REDACTED:GRAFANA_CLOUD_TOKEN]'],
140
+ [/\bglsa_[A-Za-z0-9]{32}_[a-fA-F0-9]{8}/g, '[REDACTED:GRAFANA_SERVICE_ACCOUNT]'],
141
+ [/\bsntrys_[A-Za-z0-9_=+/]{32,}/g, '[REDACTED:SENTRY_ORG_TOKEN]'],
142
+ [/\bsntryu_[A-Za-z0-9_=+/]{32,}/g, '[REDACTED:SENTRY_USER_TOKEN]'],
143
+ // ── HubSpot / Postman / Pulumi / Databricks / Prefect / Readme ──
144
+ [/\bpat-(?:na|eu)1-[a-f0-9-]{36}/g, '[REDACTED:HUBSPOT_TOKEN]'],
145
+ [/\bPMAK-[a-f0-9]{24}-[a-f0-9]{34}/g, '[REDACTED:POSTMAN_KEY]'],
146
+ [/\bpul-[a-f0-9]{40}\b/g, '[REDACTED:PULUMI_TOKEN]'],
147
+ [/\bdapi[a-f0-9]{32}(?:-[0-9]+)?\b/g, '[REDACTED:DATABRICKS_TOKEN]'],
148
+ [/\bpnu_[A-Za-z0-9]{36}\b/g, '[REDACTED:PREFECT_KEY]'],
149
+ [/\brdme_[a-z0-9]{70}\b/g, '[REDACTED:README_KEY]'],
150
+ // ── Telegram / Dropbox / Mapbox / Supabase ──
151
+ [/\b[0-9]{8,10}:AA[A-Za-z0-9_-]{33}\b/g, '[REDACTED:TELEGRAM_BOT_TOKEN]'],
152
+ [/\bsl\.[A-Za-z0-9_-]{130,}/g, '[REDACTED:DROPBOX_SHORT_LIVED]'],
153
+ [/\bsk\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[REDACTED:MAPBOX_SECRET_TOKEN]'],
154
+ [/\bsbp_[a-f0-9]{40}\b/g, '[REDACTED:SUPABASE_TOKEN]'],
155
+ // ── PlanetScale / Doppler / Fly.io / Tailscale ──
156
+ [/\bpscale_tkn_[A-Za-z0-9_=.-]{32,}/g, '[REDACTED:PLANETSCALE_TOKEN]'],
157
+ [/\bpscale_pw_[A-Za-z0-9_=.-]{32,}/g, '[REDACTED:PLANETSCALE_PASSWORD]'],
158
+ [/\bpscale_oauth_[A-Za-z0-9_=.-]{32,}/g, '[REDACTED:PLANETSCALE_OAUTH]'],
159
+ [/\bdp\.pt\.[A-Za-z0-9]{40,}/g, '[REDACTED:DOPPLER_TOKEN]'],
160
+ [/\bfo1_[A-Za-z0-9_-]{43}/g, '[REDACTED:FLYIO_TOKEN]'],
161
+ [/\btskey-auth-[A-Za-z0-9]+-[A-Za-z0-9]+/g, '[REDACTED:TAILSCALE_KEY]'],
162
+ // ── HashiCorp Vault / Terraform Cloud ──
163
+ [/\bhvs\.[A-Za-z0-9_-]{90,}/g, '[REDACTED:VAULT_TOKEN]'],
164
+ [/\bhvb\.[A-Za-z0-9_-]{90,}/g, '[REDACTED:VAULT_BATCH_TOKEN]'],
165
+ [/\b[A-Za-z0-9]{14}\.atlasv1\.[A-Za-z0-9_=-]{60,}/g, '[REDACTED:TERRAFORM_CLOUD_TOKEN]'],
166
+ // ── Dynatrace / EasyPost / Frame.io / Duffel / Flutterwave / Shippo / Plaid ──
167
+ [/\bdt0c01\.[A-Z0-9]{24}\.[A-Z0-9]{64}/g, '[REDACTED:DYNATRACE_TOKEN]'],
168
+ [/\bEZAK[A-Za-z0-9]{54}/g, '[REDACTED:EASYPOST_KEY]'],
169
+ [/\bfio-u-[A-Za-z0-9_=-]{64}/g, '[REDACTED:FRAMEIO_TOKEN]'],
170
+ [/\bduffel_(?:test|live)_[A-Za-z0-9_=-]{43}/g, '[REDACTED:DUFFEL_TOKEN]'],
171
+ [/\bFLWSECK_TEST-[a-h0-9]{32}-X/g, '[REDACTED:FLUTTERWAVE_KEY]'],
172
+ [/\bshippo_(?:live|test)_[a-fA-F0-9]{40}/g, '[REDACTED:SHIPPO_TOKEN]'],
173
+ [/\baccess-(?:sandbox|development|production)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, '[REDACTED:PLAID_TOKEN]'],
174
+ // ── Misc cloud / infra prefixes ──
175
+ [/\bLTAI[A-Za-z0-9]{20}\b/g, '[REDACTED:ALIBABA_ACCESS_KEY]'],
176
+ [/\baio_[A-Za-z0-9]{28}\b/g, '[REDACTED:ADAFRUIT_IO_KEY]'],
177
+ [/\by0_[A-Za-z0-9_-]{20,}/g, '[REDACTED:YANDEX_OAUTH]'],
178
+ // ── AI / inference providers ──
179
+ [/\bgsk_[A-Za-z0-9]{40,}/g, '[REDACTED:GROQ_KEY]'],
180
+ [/\bpplx-[A-Za-z0-9]{32,}/g, '[REDACTED:PERPLEXITY_KEY]'],
181
+ [/\bxai-[A-Za-z0-9]{32,}/g, '[REDACTED:XAI_KEY]'],
182
+ [/\bfw_[A-Za-z0-9]{24,}/g, '[REDACTED:FIREWORKS_KEY]'],
183
+ [/\bpa-[A-Za-z0-9_-]{43}\b/g, '[REDACTED:VOYAGE_KEY]'],
184
+ [/\bpcsk_[A-Za-z0-9_]{30,}/g, '[REDACTED:PINECONE_KEY]'],
185
+ [/\blsv2_pt_[a-f0-9]{32}_[a-f0-9]{10}/g, '[REDACTED:LANGSMITH_PERSONAL]'],
186
+ [/\blsv2_sk_[a-f0-9]{32}_[a-f0-9]{10}/g, '[REDACTED:LANGSMITH_SERVICE]'],
187
+ [/\bsecret-(?:test|live)-[A-Za-z0-9_=-]{36}/g, '[REDACTED:STYTCH_SECRET]'],
188
+ [/\bamzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, '[REDACTED:AWS_MWS_TOKEN]'],
189
+ // ── More infra / dev-tool prefixes ──
190
+ [/\bnapi_[a-z0-9]{30,}/g, '[REDACTED:NEON_API_KEY]'],
191
+ [/\bre_[A-Za-z0-9_]{20,}/g, '[REDACTED:RESEND_KEY]'],
192
+ [/\b[0-9]\/[0-9]{16}:[a-f0-9]{32}\b/g, '[REDACTED:ASANA_TOKEN]'],
193
+ [/\bbkua_[a-z0-9]{40}\b/g, '[REDACTED:BUILDKITE_TOKEN]'],
194
+ [/\bCCIPAT_[A-Za-z0-9]{10,}/g, '[REDACTED:CIRCLECI_TOKEN]'],
195
+ [/\brnd_[A-Za-z0-9]{30,}/g, '[REDACTED:RENDER_KEY]'],
196
+ [/\bdnkey-[a-z0-9=_-]{26}-[a-z0-9=_-]{52}/g, '[REDACTED:DEFINED_NET_KEY]'],
197
+ [/\bAccountKey=[A-Za-z0-9+/]{86,88}={0,2}/g, '[REDACTED:AZURE_STORAGE_KEY]'],
198
+ [/\bwaka_[0-9a-f-]{36}\b/g, '[REDACTED:WAKATIME_KEY]'],
199
+ [/\bsgp_[a-fA-F0-9]{40,}/g, '[REDACTED:SOURCEGRAPH_TOKEN]'],
200
+ [/\bcmVmdGtuOj[A-Za-z0-9_/+=-]{64,}/g, '[REDACTED:JFROG_TOKEN]'],
201
+ [/https:\/\/[a-f0-9]{32}@[a-z0-9.-]+\/[0-9]+/g, '[REDACTED:SENTRY_DSN]'],
202
+ // ── Context-anchored generics (require a labelled key + quoted/assigned value,
203
+ // so they fire on `name = "value"` shapes only — low false-positive. These
204
+ // run LAST so specific provider prefixes above always win first. Capture
205
+ // groups preserve the surrounding structure; only the value is redacted) ──
206
+ // Provider-anchored: require the provider name + a key-name + the value, so
207
+ // they fire only on explicit `<provider>_api_key = "<value>"` shapes.
208
+ [/(datadog[_-]?api[_-]?key["']?\s*[:=]\s*["']?)([a-f0-9]{32})/gi, '$1[REDACTED:DATADOG_KEY]'],
209
+ [/(heroku[_-]?api[_-]?key["']?\s*[:=]\s*["']?)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi, '$1[REDACTED:HEROKU_KEY]'],
210
+ [/(cloudflare[_-]?(?:api[_-]?)?(?:token|key)["']?\s*[:=]\s*["']?)([A-Za-z0-9_-]{37,40})/gi, '$1[REDACTED:CLOUDFLARE_TOKEN]'],
211
+ [/(algolia[_-]?(?:admin[_-]?)?(?:api[_-]?)?key["']?\s*[:=]\s*["']?)([a-zA-Z0-9]{32})/gi, '$1[REDACTED:ALGOLIA_KEY]'],
212
+ [/(facebook[_-]?(?:app[_-]?)?secret["']?\s*[:=]\s*["']?)([a-f0-9]{32})/gi, '$1[REDACTED:FACEBOOK_SECRET]'],
213
+ [/(linkedin[_-]?(?:client[_-]?)?secret["']?\s*[:=]\s*["']?)([A-Za-z0-9]{16,})/gi, '$1[REDACTED:LINKEDIN_SECRET]'],
214
+ [/(okta[_-]?(?:api[_-]?)?token["']?\s*[:=]\s*["']?)([A-Za-z0-9_-]{42})/gi, '$1[REDACTED:OKTA_TOKEN]'],
215
+ [/(intercom[_-]?(?:access[_-]?)?token["']?\s*[:=]\s*["']?)([A-Za-z0-9=_-]{40,})/gi, '$1[REDACTED:INTERCOM_TOKEN]'],
216
+ [/(fastly[_-]?(?:api[_-]?)?(?:token|key)["']?\s*[:=]\s*["']?)([A-Za-z0-9_-]{32})/gi, '$1[REDACTED:FASTLY_TOKEN]'],
217
+ [/(cohere[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?)([A-Za-z0-9]{40})/gi, '$1[REDACTED:COHERE_KEY]'],
218
+ [/(elevenlabs[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?)([a-f0-9]{32})/gi, '$1[REDACTED:ELEVENLABS_KEY]'],
219
+ [/(deepgram[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?)([a-f0-9]{40})/gi, '$1[REDACTED:DEEPGRAM_KEY]'],
220
+ [/(mistral[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?)([A-Za-z0-9]{32})/gi, '$1[REDACTED:MISTRAL_KEY]'],
221
+ [/(twitch[_-]?(?:client[_-]?)?secret["']?\s*[:=]\s*["']?)([a-z0-9]{30})/gi, '$1[REDACTED:TWITCH_SECRET]'],
222
+ [/(coinbase[_-]?(?:api[_-]?)?(?:secret|key)["']?\s*[:=]\s*["']?)([A-Za-z0-9]{32,})/gi, '$1[REDACTED:COINBASE_KEY]'],
223
+ [/(snyk[_-]?(?:api[_-]?)?(?:token|key)["']?\s*[:=]\s*["']?)([0-9a-f-]{36})/gi, '$1[REDACTED:SNYK_TOKEN]'],
224
+ [/(azure[_-]?(?:ad[_-]?)?client[_-]?secret["']?\s*[:=]\s*["']?)([A-Za-z0-9._~-]{30,})/gi, '$1[REDACTED:AZURE_AD_SECRET]'],
225
+ [/(mux[_-]?token[_-]?secret["']?\s*[:=]\s*["']?)([A-Za-z0-9+/=]{40,})/gi, '$1[REDACTED:MUX_SECRET]'],
226
+ // AWS secret access key (40-char base64 only meaningful in context)
227
+ [/((?:aws_?)?secret_?access_?key["']?\s*[:=]\s*["']?)([A-Za-z0-9/+]{40})/gi, '$1[REDACTED:AWS_SECRET_KEY]'],
228
+ // Credentials embedded in a URL (DB conn strings + HTTP basic auth):
229
+ // scheme://user:PASSWORD@host → keeps scheme/user/@host, redacts password.
230
+ // Covers postgres / mysql / mongodb+srv / redis / amqp / http(s) / etc.
231
+ [/\b([a-z][a-z0-9+.-]*:\/\/[^:/?#\s@]+:)([^@/?#\s]+)(@)/gi, '$1[REDACTED:URL_CREDENTIALS]$3'],
232
+ // Generic quoted secret/API-key/token assignments.
233
+ [/(\b(?:api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key)["']?\s*[:=]\s*["'])([A-Za-z0-9._+/=-]{16,})(["'])/gi, '$1[REDACTED:GENERIC_SECRET]$3'],
234
+ // Generic quoted password assignments (require quotes + length to limit FP).
235
+ [/(\b(?:password|passwd|pwd)["']?\s*[:=]\s*["'])([^"'\s]{8,})(["'])/gi, '$1[REDACTED:PASSWORD]$3'],
236
+ // ── Private-key PEM blocks (RSA / EC / DSA / OPENSSH / PGP / encrypted /
237
+ // plain, incl. GCP service-account JSON `private_key` with escaped \n) ──
238
+ [/-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]+?-----END (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, '[REDACTED:PRIVATE_KEY]'],
239
+ ];
240
+ // ─── High-entropy catch-all ────────────────────────────────────────────────
241
+ // Last-resort net for PREFIXLESS keys the patterns miss. Deliberately
242
+ // CONSERVATIVE: it must never redact git SHAs, UUIDs, hex colors, file paths,
243
+ // hash-named files, numeric ids, build-ids / content-hashes in prose, or
244
+ // ordinary prose. It fires only on a key-sized window of an unbroken
245
+ // base64url-ish run that mixes letters AND digits, spans ≥3 character classes,
246
+ // is not pure hex, not a UUID, and clears an entropy floor — AND is neither a
247
+ // filename stem (`<run>.<ext>`) nor a path component (preceded by `/` or `\`).
248
+ const HEX_RE = /^[0-9a-fA-F]+$/;
249
+ const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
250
+ // Candidate run: base64url + base64-pad alphabet, NO `/` or `.` (so file paths,
251
+ // URLs and dotted version strings break into short sub-tokens). 32–120 chars: a
252
+ // key-sized window — long enough to exclude most identifiers, short enough to
253
+ // skip multi-KB base64 data blobs (which are not credentials).
254
+ const ENTROPY_CANDIDATE_RE = /[A-Za-z0-9_+=-]{32,120}/g;
255
+ const ENTROPY_MIN_BITS = 4.0;
256
+ // Tail test: a candidate immediately followed by `.<short ext>` is a filename
257
+ // stem (a8Kd…Gh3.md, Xy7…Jk.json), not a secret. The candidate run stops at the
258
+ // `.`, so this matches against the text that FOLLOWS the matched run.
259
+ const FILE_EXT_TAIL_RE = /^\.[A-Za-z0-9]{1,8}\b/;
260
+ /** Shannon entropy of a string, in bits per character. */
261
+ function shannonEntropy(s) {
262
+ if (!s)
263
+ return 0;
264
+ const freq = {};
265
+ for (const ch of s)
266
+ freq[ch] = (freq[ch] || 0) + 1;
267
+ let bits = 0;
268
+ for (const ch in freq) {
269
+ const p = freq[ch] / s.length;
270
+ bits -= p * Math.log2(p);
271
+ }
272
+ return bits;
273
+ }
274
+ /** Count distinct character classes (lower / upper / digit / symbol) in a token.
275
+ * A real prefixless key mixes ≥3; a non-secret build-id or content hash in prose
276
+ * is usually 1–2 (e.g. all-lowercase + digits), so requiring ≥3 clears those
277
+ * false positives without dropping genuine mixed-case-plus-digit keys. */
278
+ function charClassCount(token) {
279
+ let n = 0;
280
+ if (/[a-z]/.test(token))
281
+ n++;
282
+ if (/[A-Z]/.test(token))
283
+ n++;
284
+ if (/[0-9]/.test(token))
285
+ n++;
286
+ if (/[_+=-]/.test(token))
287
+ n++;
288
+ return n;
289
+ }
290
+ /** True when a single token looks like a high-entropy secret (see thresholds). */
291
+ function isHighEntropyToken(token) {
292
+ if (token.length < 32 || token.length > 120)
293
+ return false;
294
+ if (HEX_RE.test(token))
295
+ return false; // git SHA / MD5 / hex color / UUID-sans-hyphen
296
+ if (UUID_RE.test(token))
297
+ return false; // canonical UUID
298
+ if (!/[A-Za-z]/.test(token))
299
+ return false; // pure-numeric ids
300
+ if (!/[0-9]/.test(token))
301
+ return false; // prose / camelCase words (no digit)
302
+ if (charClassCount(token) < 3)
303
+ return false; // single-/double-class ids (build-ids, content hashes in prose)
304
+ return shannonEntropy(token) >= ENTROPY_MIN_BITS;
305
+ }
306
+ /** Redact prefixless high-entropy tokens. The entropy catch-all (pass 3).
307
+ * A high-entropy run is NOT redacted when it is a filename stem (immediately
308
+ * followed by `.<short ext>`) or a path component (immediately preceded by `/`
309
+ * or `\`) — a hash-named file or a path segment is an identifier, not a secret.
310
+ * Those guards need the surrounding text, so they live here, not in the
311
+ * token-only `isHighEntropyToken`. */
312
+ function redactHighEntropy(text) {
313
+ if (!text)
314
+ return text;
315
+ return text.replace(ENTROPY_CANDIDATE_RE, (m, offset, str) => {
316
+ if (!isHighEntropyToken(m))
317
+ return m;
318
+ const before = offset > 0 ? str[offset - 1] : '';
319
+ if (before === '/' || before === '\\')
320
+ return m; // path component
321
+ if (FILE_EXT_TAIL_RE.test(str.slice(offset + m.length)))
322
+ return m; // filename stem
323
+ return '[REDACTED:HIGH_ENTROPY]';
324
+ });
325
+ }
326
+ // ─── Pure scrubbing API ────────────────────────────────────────────────────
327
+ /** Scrub a string. Three passes, in order:
328
+ * 1. caller-supplied value map (e.g. .env values) — exact replace, the map is
329
+ * expected longest-first so longer secrets win before their substrings;
330
+ * 2. provider SECRET_PATTERNS;
331
+ * 3. high-entropy catch-all.
332
+ * Filesystem-free — the value map is the caller's job (the CLI hook builds it
333
+ * from .env; the server passes none). */
334
+ function scrubString(text, valueMap) {
335
+ if (!text)
336
+ return text;
337
+ let out = text;
338
+ // Pass 1: caller value-map replacement (deterministic, names the var).
339
+ if (valueMap) {
340
+ for (const [value, replacement] of valueMap) {
341
+ if (value && out.includes(value))
342
+ out = out.split(value).join(replacement);
343
+ }
344
+ }
345
+ // Pass 2: pattern-based detection for tokens not in any value map.
346
+ for (const [pattern, replacement] of exports.SECRET_PATTERNS) {
347
+ out = out.replace(pattern, replacement);
348
+ }
349
+ // Pass 3: high-entropy catch-all for prefixless keys.
350
+ out = redactHighEntropy(out);
351
+ return out;
352
+ }
353
+ /** Recursively scrub all string leaves of an object / array, preserving shape. */
354
+ function scrubObject(obj, valueMap) {
355
+ if (typeof obj === 'string')
356
+ return scrubString(obj, valueMap);
357
+ if (Array.isArray(obj))
358
+ return obj.map((o) => scrubObject(o, valueMap));
359
+ if (obj && typeof obj === 'object') {
360
+ const out = {};
361
+ for (const k of Object.keys(obj)) {
362
+ out[k] = scrubObject(obj[k], valueMap);
363
+ }
364
+ return out;
365
+ }
366
+ return obj;
367
+ }
368
+ //# sourceMappingURL=secret-scrubber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-scrubber.js","sourceRoot":"","sources":["../src/secret-scrubber.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greprag",
3
- "version": "5.38.0",
3
+ "version": "5.40.0",
4
4
  "description": "GrepRAG — agent memory for Claude Code, Codex, and OpenCode.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -125,18 +125,21 @@ Aliases (silent back-compat): `greprag memory briefing` → `recap` (renamed v5.
125
125
 
126
126
  **Codex live inbox push requires the startup watcher.** Hooks only fire at Codex turn/tool/session boundaries. Public installs should use `greprag init --codex --tenant-id <handle> --install-watcher`; run `greprag codex doctor --wake-test` to inspect state, and use `greprag codex watch --session <id>` only for foreground testing. The sidecar listens to GrepRAG inbox SSE and wakes Codex via `codex exec resume`. If the wake-action probe fails, describe live push as notification-only and rely on turn-bound inbox steering for reliable delivery.
127
127
 
128
- **ABOUT TO SEND TO A `@gmail.com` / `@anthropic.com` / REAL EMAIL ADDRESS? STOP — `users.email` IS NEVER A ROUTING ADDRESS.** Use the numeric handle (`1834729@greprag.com`) or claimed vanity alias (`travis@greprag.com`). If you don't know the recipient's handle, ASK — don't guess from their email. adr: adr/numeric-handles.md. Full grammar: `docs/inbox.md § address`.
128
+ **ABOUT TO `greprag send` TO A `@gmail.com` / `@anthropic.com` / REAL EMAIL ADDRESS? STOP — for `inbox`/`send` (internal cross-session messaging), `users.email` IS NEVER A ROUTING ADDRESS.** Use the numeric handle (`1834729@greprag.com`) or claimed vanity alias (`travis@greprag.com`). If you don't know the recipient's handle, ASK — don't guess from their email. adr: adr/numeric-handles.md. Full grammar: `docs/inbox.md § address`.
129
+
130
+ **BUT IF THE USER SAYS "EMAIL X" / "SEND A REAL EMAIL" — USE `greprag email send`, NOT `greprag send`.** `greprag email send --to <real-addr> --from <handle>@greprag.com --subject "s" (--body "t" | --body-file <f|->)` is the agent's REAL outbound SMTP mailbox — it delivers to actual inboxes (gmail, etc.), with `--attach`/`--cc`/`--bcc`/`--html-file`. `greprag send` is internal-only and never leaves greprag. The two read identically — this collision is a known trap. `email pending`/`email pull` drains inbound. Full reference: `docs/email.md`.
129
131
 
130
132
  ## Reference index
131
133
 
132
134
  - `docs/setup.md` — codex · claude-code · opencode · auth · hooks · conventions · permissions · channels · anchor · bulk-register
133
135
  - `docs/platforms.md` — exact platform paths for Claude Code · Codex · OpenCode
134
136
  - `docs/per-project-flags.md` — flip `memory_capture` / `session_start_recap` / `inbox_notify`
135
- - `docs/inbox.md` — `greprag send`, `greprag inbox`, address grammar, retract
137
+ - `docs/inbox.md` — `greprag send`, `greprag inbox`, address grammar, retract (internal messaging)
138
+ - `docs/email.md` — `greprag email send`/`pending`/`pull` — REAL outbound + inbound SMTP (distinct from `send`)
136
139
  - `docs/inbox-watch.md` — SSE watcher patterns, liveness model, parent-side / post-send patterns
137
140
  - `docs/doctor.md` — `greprag doctor` (identity drift, orphan consolidation)
138
141
  - `docs/corpus.md` — upload + search arbitrary text (books, codebases, voice samples)
139
142
  - `docs/discord-handoff.md` — Commander DM bridge (`greprag discord pair/handoff`)
140
143
  - `docs/memory-advanced.md` — raw curl, `/v1/memory/by-period` `--type` values, naming note
141
- - `docs/lore.md` — seed + query project-specific learnings
144
+ - `docs/fix.md` — log + query project-specific fixes (the Mechanic's queue)
142
145
  - `docs/discover.md` — cross-project advisor lookup (`greprag discover`)
@@ -18,6 +18,21 @@ greprag corpus upload <file-or-url> [--name "Display Name"] [--kind book]
18
18
  - File or URL must be plain text or markdown. PDFs: ask user to convert first.
19
19
  - Cost: one round-trip to ingest + N async Gemini Flash-Lite calls (one per substantive node, drained every 2 min). Read-time cost: $0.
20
20
 
21
+ ## Recipe: ingest SPA-rendered API docs (no public OpenAPI download)
22
+
23
+ Modern API doc sites (ReadMe, Stoplight, Redoc, Mintlify, GitBook, custom SPAs) render client-side: `curl`/WebFetch return an empty shell, there's no linked `openapi.json`, and the endpoint catalog is usually NOT in the JS bundle. **Don't scrape the DOM.** Almost all of them are generated from OpenAPI and fetch it from an internal — often public — data API. Find that, ingest the spec.
24
+
25
+ 1. **Find the data API.** Cold-load a reference page in the browser with network capture already armed (capture clears on cross-domain nav, so arm while on-domain then hard-reload). Inspect `performance.getEntriesByType('resource')` for an internal `/api/` JSON feed — that's the catalog/spec source. The spec is fetched once at hard load, not on SPA route changes.
26
+ 2. **Pull the catalog**, then **the per-resource spec.** Both are often public (test with `curl`, no cookies). Watch for explicit version codes — generic `?version=latest` may 400.
27
+ - Procore (worked end-to-end): `GET developers.procore.com/api/v1/resource_groups` → 588 resources (each w/ `resource_name_id` + `versions`); then `GET .../api/v1/resource_groups/resource/{rid}?version=rest_v1.1` → real OpenAPI 3.0 per resource.
28
+ 3. **Render each spec → compact markdown**: per endpoint, emit `METHOD /path`, summary, params, and request/response field schemas with **required** flags + enums + descriptions (resolve `$ref` against the spec root). Far more searchable + smaller than raw OpenAPI JSON.
29
+ 4. **Ingest as one consolidated file.** `--index` wants an `llms.txt`/`sitemap.xml` URL the vendor doesn't publish, so concatenate all rendered specs into one file (the `llms-full.txt` model) and `greprag corpus upload <file>.md --raw --name <vendor>-docs`. Use `--raw` (reference docs — search terms already match the text).
30
+ 5. **Refresh** = rerun the build script + re-upload (static file doesn't auto-refresh).
31
+
32
+ Keep the build script in the consuming repo (`scripts/build-<vendor>-corpus.mjs`) so refresh/rescope is one command. Scope to the resources you actually integrate with — full vendor catalogs are often huge and mostly irrelevant.
33
+
34
+ > Future greprag enhancement: teach `corpus --index` to accept a JSON catalog + a member-URL selector (JSONPath), so OpenAPI-backed doc APIs auto-refresh as proper index stores instead of static files.
35
+
21
36
  ## Search — chain protocol
22
37
 
23
38
  Searches are agent-driven. You decompose the user's intent into 1–3 lexical queries.
@@ -21,7 +21,7 @@ Response shape:
21
21
  }
22
22
  ```
23
23
 
24
- `anchor: "registered"` means the project was registered via `/v1/inbox/projects/register` (has a row in the `projects` table — inbox addressing as `<email>/<project_name>` works).
24
+ `anchor: "registered"` means the project was registered via `/v1/inbox/projects/register` (has a row in the `projects` table — so `--project <name>` filters on `greprag inbox` / `inbox watch` resolve it). A project is NOT a send target, though — project broadcasts were removed (2026-06-07); send to a session or the bare front desk.
25
25
 
26
26
  `anchor: "fallback"` means the project is only known via memory-store metadata — still has memory, but no registry entry. Its name may be the path-derived basename and inbox-by-name resolution will not find it.
27
27
 
@@ -0,0 +1,46 @@
1
+ # greprag email — the agent's real SMTP mailbox
2
+
3
+ `greprag email` is **real outbound + inbound email** from your greprag address (e.g. `travis@greprag.com`). This is distinct from `greprag send` / `greprag inbox`, which is internal cross-session messaging that never leaves greprag.
4
+
5
+ **The trap:** "email me" and "send X" sound identical. Only `greprag email send` produces an actual email in a real inbox. `greprag send --to travis@greprag.com` lands on a greprag front desk, NOT in gmail.
6
+
7
+ ## Send a real email
8
+
9
+ ```
10
+ greprag email send --to <real-addr> \
11
+ --from <handle>@greprag.com \
12
+ --subject "<subject>" \
13
+ (--body "<text>" | --body-file <file|->)
14
+ ```
15
+
16
+ Flags:
17
+ - `--from <handle>@greprag.com` — send AS one of YOUR OWN greprag handles. The server rejects any address that isn't yours — you cannot forge a From. Defaults to your tenant address if omitted.
18
+ - `--body-file <f>` — read body from a file; `--body-file -` reads from stdin (best for long/multiline bodies — avoids shell-quoting pain).
19
+ - `--html-file <f>` — HTML alternative body.
20
+ - `--attach <path>` — attach a local file (repeatable). Gmail's combined limit is 25 MB; larger → Drive link.
21
+ - `--cc <addr>` / `--bcc <addr>` — repeatable / comma-sep.
22
+ - `--reply-to <addr>` — Reply-To header.
23
+
24
+ Returns a `message-id` and writes an egress audit `.eml` on success.
25
+
26
+ ### Long body via heredoc (the reliable pattern)
27
+
28
+ ```bash
29
+ cat > /tmp/body.txt <<'EOF'
30
+ Multi-line body here.
31
+ No shell-quoting headaches.
32
+ EOF
33
+ greprag email send --to someone@gmail.com --from travis@greprag.com \
34
+ --subject "Subject" --body-file /tmp/body.txt
35
+ ```
36
+
37
+ ## Inbound
38
+
39
+ - `greprag email` or `greprag email pending` — list pending front-desk email (envelope only).
40
+ - `greprag email pull --id <record>` — pull ONE record's attachments to disk.
41
+ - `greprag email pull --all-pending [--to <dir>]` — pull every pending record's attachments. Save dir resolves: `--to` > `$GREPRAG_EMAIL_DIR` > per-project anchor `email_dir` > `~/.greprag/email/<project>`.
42
+ - Auto-save: set `email_autosave=true` in `.greprag/project.json` (optionally `email_dir`) — the per-turn mail hook auto-pulls new attachments each turn.
43
+
44
+ ## Sending rule
45
+
46
+ Sending email is an outward-facing action — confirm with the user before sending unless they've explicitly asked for it in this turn. (Here they asked → send.)
@@ -0,0 +1,86 @@
1
+ # Fix (per-project friction queue — `greprag fix`)
2
+
3
+ A fix = a rough spot worth repairing. Project-specific emergent knowledge — gotchas, discovered constraints, drift-prone observations, plus raw smells (open fixes) awaiting digestion. Distinct from static project knowledge (which belongs in CLAUDE.md / docs / code structure). Reviewed via `/mechanic`.
4
+
5
+ Whenever you waste tokens *discovering* something a future agent shouldn't have to re-discover, log it. Discovery is fine the first time — the failure mode is repeating it every time a chip spawns or a new session opens.
6
+
7
+ ## When to log
8
+
9
+ Three signatures of discovery waste:
10
+
11
+ - **Search cascade.** ≥3 `Glob` / `Grep` calls to find a path that should be obvious (where do migrations live? where's the inbox table? which file owns the CLI dispatch?). Log with `--scope chip-startup --repaired`.
12
+ - **Schema archaeology.** ≥3 file `Read`s to reconstruct the shape of a data type, a JSONB column, or an API request body. Log the shape (1–3 lines) with `--scope <subsystem>-touch --repaired`.
13
+ - **Trial-and-error env probing.** Multiple `Bash` attempts at the same env operation. Log the working invocation with `--scope env --repaired`.
14
+
15
+ ## How to log
16
+
17
+ Open a raw fix (the Mechanic digests it later):
18
+
19
+ ```bash
20
+ greprag fix log "<text>" [--project <name>]
21
+ ```
22
+
23
+ Record a known durable rule directly (`--repaired` skips the digestion step — use when you already know the canonical fix):
24
+
25
+ ```bash
26
+ greprag fix log "<one-sentence learning, optionally with a file path>" --repaired --scope <scope> [--project <name>]
27
+ ```
28
+
29
+ Examples:
30
+ ```bash
31
+ greprag fix log "Migrations live at repo root: migrations/<NNN>_<name>.sql. Apply with node scripts/apply-migration.cjs migrations/<file>.sql." --repaired --scope chip-startup
32
+
33
+ greprag fix log "Inbox storage uses JSONB metadata on nodes (store kind='inbox'). No inbox_messages table — dropped in migration 035. See packages/core/src/inbox.ts." --repaired --scope inbox-touch
34
+
35
+ greprag fix log "Build everything from repo root: npm run build (Turborepo). Forced rebuild: npm run build -- --force." --repaired --scope general
36
+ ```
37
+
38
+ ## Scope naming
39
+
40
+ Free-form strings — no enum. Check what's already in use first:
41
+
42
+ ```bash
43
+ greprag fix scopes [--project <name>]
44
+ ```
45
+
46
+ Conventional scopes:
47
+ - `chip-startup` — what a freshly-spawned chip needs in its first 5 turns. Layout, build commands, test runners, key file paths.
48
+ - `general` — applies to almost any session in this project.
49
+ - `<subsystem>-touch` — fixes that matter only when editing a specific subsystem (`inbox-touch`, `episodic-touch`, `enrichment-touch`).
50
+ - `env` — environment / credentials / DB connection gotchas.
51
+
52
+ Pick the narrowest scope that still applies. A fix about migration paths is `chip-startup` (every chip needs it); one about how the hourly compactor's prompt versioning works is `episodic-touch` (only relevant if you're editing that subsystem).
53
+
54
+ ## Reading fixes back
55
+
56
+ ```bash
57
+ greprag fix list # open queue (raw smells awaiting digestion)
58
+ greprag fix list --repaired --format markdown # durable rules grouped by scope (human review)
59
+ greprag fix search "how do migrations apply" --limit 5 # lexical-rank across scopes
60
+ greprag fix search "session routing" --scope inbox-touch # lexical-rank within a scope
61
+ greprag fix delete <nodeId> # prune stale entry
62
+ ```
63
+
64
+ `--format markdown` on `fix search` returns a numbered list with no decoration. `fix list --repaired` prints entries grouped under `## <scope>` headings (it ignores `--scope`/`--limit`/`--format markdown`) — slice the scope group you need into a `**Project Fixes**` block when spawning a chip.
65
+
66
+ ## Repair (digest an open fix into a durable rule)
67
+
68
+ ```bash
69
+ greprag fix repair <id> "<durable rule>" [--scope <s>]
70
+ ```
71
+
72
+ A `repair` records the durable rule (kept, browsable via `fix list --repaired`) **and** closes the open fix in one step. To remove a fix without keeping a rule, use `fix delete`.
73
+
74
+ ## Deliberate review
75
+
76
+ Fixes decay as code moves — paths change, conventions die. Run `/mechanic` periodically (especially after a refactor or rename) to audit drift, mine episodic memory for newly-emerged learnings, and promote project-agnostic entries to global rules.
77
+
78
+ ## Chip-spawn pull pattern
79
+
80
+ When composing a `spawn_task` chip prompt, pull `chip-startup` fixes into the prompt before dispatching. Full convention: `~/.claude/docs/chip-spawn.md`. One-liner:
81
+
82
+ ```bash
83
+ greprag fix list --repaired --project <project> --format markdown
84
+ ```
85
+
86
+ Take the `## chip-startup` section and wrap it in a `**Project Fixes**` block at the top of the prompt. (Don't reach for `fix search` here: it requires a positional query that lexically *filters* — a guessed query drops entries — and the query-less `--scope` form mis-parses `--scope` as the query.) Skip the block when there is no `chip-startup` group.