codiedev 0.9.79 → 0.9.81

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 (39) hide show
  1. package/assets/opencode-plugin.mjs +6 -0
  2. package/assets/session-key.mjs +48 -0
  3. package/dist/cli.js +113 -9
  4. package/dist/cliSession.d.ts +108 -0
  5. package/dist/cliSession.js +513 -0
  6. package/dist/codie.js +21 -1
  7. package/dist/commands/agentTemplates.generated.d.ts +11 -0
  8. package/dist/commands/agentTemplates.generated.js +139 -0
  9. package/dist/commands/agents.d.ts +7 -0
  10. package/dist/commands/agents.js +1130 -0
  11. package/dist/commands/agentsSdk.generated.d.ts +4 -0
  12. package/dist/commands/agentsSdk.generated.js +10 -0
  13. package/dist/commands/scrub.d.ts +11 -0
  14. package/dist/commands/scrub.js +102 -0
  15. package/dist/configDir.d.ts +9 -0
  16. package/dist/configDir.js +25 -1
  17. package/dist/connect.js +45 -2
  18. package/dist/connectFlow.d.ts +21 -0
  19. package/dist/connectFlow.js +41 -0
  20. package/dist/hook.js +25 -0
  21. package/dist/knownSecrets.d.ts +43 -0
  22. package/dist/knownSecrets.js +241 -0
  23. package/dist/launcher.d.ts +7 -2
  24. package/dist/launcher.js +36 -2
  25. package/dist/localScrub.d.ts +172 -0
  26. package/dist/localScrub.js +820 -0
  27. package/dist/localScrubReport.d.ts +83 -0
  28. package/dist/localScrubReport.js +268 -0
  29. package/dist/managedPi.js +18 -2
  30. package/dist/mcp.js +3 -0
  31. package/dist/scrub/extraRules.d.ts +4 -0
  32. package/dist/scrub/extraRules.js +17 -0
  33. package/dist/scrub/rules.d.ts +86 -0
  34. package/dist/scrub/rules.js +645 -0
  35. package/dist/scrub/scrubProviderRules.d.ts +14 -0
  36. package/dist/scrub/scrubProviderRules.js +149 -0
  37. package/dist/utils.d.ts +2 -0
  38. package/dist/utils.js +9 -7
  39. package/package.json +5 -4
@@ -0,0 +1,83 @@
1
+ export type SessionStatus = "removed" | "not_removed";
2
+ export type NotRemovedReason = "cursor_chat_store";
3
+ export interface KeyHit {
4
+ rule: string;
5
+ /** Last 4 alphanumerics, same as the server's secretFindings hint. */
6
+ hint: string;
7
+ count: number;
8
+ }
9
+ export interface SessionDetection {
10
+ harness: string;
11
+ /** Stable id of the session within its harness (uuid, file name, chat id). */
12
+ sessionKey: string;
13
+ title?: string;
14
+ cwd?: string;
15
+ keys: KeyHit[];
16
+ status: SessionStatus;
17
+ reason?: NotRemovedReason;
18
+ }
19
+ /** Keys in `text` as (rule, last 4, count). Never returns a value. */
20
+ export declare function keysIn(text: string): KeyHit[];
21
+ export declare function mergeKeys(a: KeyHit[], b: KeyHit[]): KeyHit[];
22
+ /** Title and working folder of a session file, from its cleaned text. */
23
+ export declare function describeText(kind: string, cleaned: string): {
24
+ title?: string;
25
+ cwd?: string;
26
+ };
27
+ /** Which session a file belongs to, per harness layout. */
28
+ export declare function sessionKeyOf(harness: string, file: string): string;
29
+ /** Drop titles and folders for sessions the person excluded from capture.
30
+ * With any exclusion set, a session whose folder we can't tell is treated
31
+ * as excluded too. */
32
+ export declare function redactExcluded(s: SessionDetection, ignored: string[] | undefined): SessionDetection;
33
+ /** A random id for this machine, kept in the scrub's state folder. */
34
+ export declare function machineId(dir: string): string;
35
+ export interface ReportPayload {
36
+ machine: {
37
+ id: string;
38
+ name: string;
39
+ };
40
+ at: number;
41
+ complete: boolean;
42
+ scanned: number;
43
+ cleaned: number;
44
+ redactions: number;
45
+ errors: number;
46
+ skippedRecent: number;
47
+ skippedLarge: number;
48
+ byHarness: Array<{
49
+ harness: string;
50
+ files: number;
51
+ redactions: number;
52
+ }>;
53
+ leftInCursor: {
54
+ keys: number;
55
+ chats: number;
56
+ };
57
+ sessions: Array<Omit<SessionDetection, "cwd">>;
58
+ }
59
+ export declare function buildReport(input: {
60
+ machineId: string;
61
+ at: number;
62
+ complete: boolean;
63
+ summary: {
64
+ scanned: number;
65
+ cleaned: number;
66
+ redactions: number;
67
+ errors: number;
68
+ skippedRecent: number;
69
+ skippedLarge: number;
70
+ byHarness: Partial<Record<string, {
71
+ files: number;
72
+ redactions: number;
73
+ }>>;
74
+ leftInCursor: {
75
+ keys: number;
76
+ chats: number;
77
+ };
78
+ };
79
+ sessions: SessionDetection[];
80
+ ignoredPaths?: string[];
81
+ }): ReportPayload;
82
+ /** Best-effort POST to Codiedev. Never throws. */
83
+ export declare function sendReport(backendUrl: string, token: string, payload: ReportPayload, cliVersion: string, fetchImpl?: typeof fetch): Promise<boolean>;
@@ -0,0 +1,268 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.keysIn = keysIn;
37
+ exports.mergeKeys = mergeKeys;
38
+ exports.describeText = describeText;
39
+ exports.sessionKeyOf = sessionKeyOf;
40
+ exports.redactExcluded = redactExcluded;
41
+ exports.machineId = machineId;
42
+ exports.buildReport = buildReport;
43
+ exports.sendReport = sendReport;
44
+ // What the local scrub tells Codiedev, for the Security tab's audit: per
45
+ // session, when keys were found, which (type + last 4, never the key), and
46
+ // whether they were removed or left (and why). Titles are the harness's own
47
+ // title or the first prompt, taken from the CLEANED text so a title never
48
+ // carries a key, and dropped for sessions in folders excluded from capture.
49
+ const crypto = __importStar(require("crypto"));
50
+ const fs = __importStar(require("fs"));
51
+ const os = __importStar(require("os"));
52
+ const path = __importStar(require("path"));
53
+ const rules_1 = require("./scrub/rules");
54
+ const extraRules_1 = require("./scrub/extraRules");
55
+ const captureExclusions_1 = require("./captureExclusions");
56
+ const MAX_KEYS_PER_SESSION = 20;
57
+ const TITLE_LEN = 80;
58
+ /** Keys in `text` as (rule, last 4, count). Never returns a value. */
59
+ function keysIn(text) {
60
+ const { redactions } = (0, extraRules_1.scrub)(text);
61
+ if (redactions.length === 0)
62
+ return [];
63
+ const hints = new Map();
64
+ // secretFindings gives each redaction's hint the same way the server does.
65
+ for (const r of redactions) {
66
+ const [f] = (0, rules_1.secretFindings)(text, [r]);
67
+ if (!f)
68
+ continue;
69
+ const k = `${f.rule}\u0000${f.hint}`;
70
+ const hit = hints.get(k) ?? { rule: f.rule, hint: f.hint, count: 0 };
71
+ hit.count += 1;
72
+ hints.set(k, hit);
73
+ }
74
+ return [...hints.values()].slice(0, MAX_KEYS_PER_SESSION);
75
+ }
76
+ function mergeKeys(a, b) {
77
+ const out = new Map(a.map((k) => [`${k.rule}\u0000${k.hint}`, { ...k }]));
78
+ for (const k of b) {
79
+ const id = `${k.rule}\u0000${k.hint}`;
80
+ const cur = out.get(id);
81
+ if (cur)
82
+ cur.count += k.count;
83
+ else
84
+ out.set(id, { ...k });
85
+ }
86
+ return [...out.values()].slice(0, MAX_KEYS_PER_SESSION);
87
+ }
88
+ function tidy(s) {
89
+ const t = s.replace(/\s+/g, " ").trim();
90
+ if (!t)
91
+ return undefined;
92
+ return t.length > TITLE_LEN ? `${t.slice(0, TITLE_LEN - 1)}…` : t;
93
+ }
94
+ /** Injected context the harnesses put in user-role messages. */
95
+ const NOT_A_PROMPT = /^(?:<|# AGENTS\.md|Caveat:|\[Request interrupted)/;
96
+ function textOf(content) {
97
+ if (typeof content === "string")
98
+ return content;
99
+ if (Array.isArray(content)) {
100
+ for (const part of content) {
101
+ const p = part;
102
+ if (p && typeof p.text === "string" && (p.type === undefined || /text/.test(String(p.type))))
103
+ return p.text;
104
+ }
105
+ }
106
+ return undefined;
107
+ }
108
+ /** The first thing the person typed, across every harness's JSONL shape. */
109
+ function firstPrompt(obj) {
110
+ const msg = obj.message;
111
+ const payload = obj.payload;
112
+ const data = obj.data;
113
+ const candidates = [];
114
+ if (msg?.role === "user")
115
+ candidates.push(msg.content); // Claude Code, pi
116
+ if (obj.role === "user")
117
+ candidates.push(obj.message?.content ?? obj.content); // Cursor
118
+ if (payload?.type === "message" && payload.role === "user")
119
+ candidates.push(payload.content); // Codex
120
+ if (obj.type === "user.message")
121
+ candidates.push(data?.content); // Copilot
122
+ for (const c of candidates) {
123
+ const t = textOf(c);
124
+ if (t && !NOT_A_PROMPT.test(t.trimStart()))
125
+ return t;
126
+ }
127
+ return undefined;
128
+ }
129
+ /** Title and working folder of a session file, from its cleaned text. */
130
+ function describeText(kind, cleaned) {
131
+ let title;
132
+ let named;
133
+ let cwd;
134
+ if (kind === "yaml") {
135
+ for (const line of cleaned.split(/\r?\n/)) {
136
+ const m = line.match(/^(summary|name|cwd):\s*(.*)$/);
137
+ if (!m)
138
+ continue;
139
+ const v = m[2].trim().replace(/^(["'])(.*)\1$/, "$2");
140
+ if (m[1] === "cwd")
141
+ cwd ??= v;
142
+ else
143
+ named ??= v;
144
+ }
145
+ return { title: named ? tidy(named) : undefined, cwd };
146
+ }
147
+ const docs = [];
148
+ if (kind === "jsonl") {
149
+ const lines = cleaned.split("\n");
150
+ for (let i = 0; i < lines.length && i < 2000; i++) {
151
+ if (!lines[i].trim())
152
+ continue;
153
+ try {
154
+ docs.push(JSON.parse(lines[i]));
155
+ }
156
+ catch { /* not JSON */ }
157
+ }
158
+ }
159
+ else if (kind === "json") {
160
+ try {
161
+ const d = JSON.parse(cleaned);
162
+ if (Array.isArray(d))
163
+ docs.push(...d);
164
+ else
165
+ docs.push(d);
166
+ }
167
+ catch { /* not JSON */ }
168
+ }
169
+ for (const d of docs) {
170
+ const o = d;
171
+ if (!o || typeof o !== "object")
172
+ continue;
173
+ // Harness-provided titles win over the first prompt.
174
+ if (!named && o.type === "summary" && typeof o.summary === "string")
175
+ named = o.summary; // Claude Code
176
+ if (!named && typeof o.thread_name === "string")
177
+ named = o.thread_name; // Codex
178
+ if (!cwd) {
179
+ const c = o.cwd ?? o.payload?.cwd;
180
+ if (typeof c === "string")
181
+ cwd = c;
182
+ }
183
+ if (!title) {
184
+ // OpenCode capture copy: [{ info: { role }, parts: [{ type: "text", text }] }]
185
+ const info = o.info;
186
+ const t = info?.role === "user" ? textOf(o.parts) : firstPrompt(o);
187
+ if (t)
188
+ title = t;
189
+ }
190
+ }
191
+ return { title: tidy(named ?? title ?? ""), cwd };
192
+ }
193
+ /** Which session a file belongs to, per harness layout. */
194
+ function sessionKeyOf(harness, file) {
195
+ const base = path.basename(file);
196
+ if (base === "history.jsonl")
197
+ return "history";
198
+ if (harness === "copilot")
199
+ return path.basename(path.dirname(file)); // session-state/<id>/…
200
+ const uuid = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
201
+ if (uuid)
202
+ return uuid[1];
203
+ return base.replace(/\.(jsonl|json|ya?ml|txt)$/, "");
204
+ }
205
+ /** Drop titles and folders for sessions the person excluded from capture.
206
+ * With any exclusion set, a session whose folder we can't tell is treated
207
+ * as excluded too. */
208
+ function redactExcluded(s, ignored) {
209
+ if (!ignored?.length)
210
+ return s;
211
+ if (s.cwd && !(0, captureExclusions_1.isCaptureIgnored)(s.cwd, ignored))
212
+ return s;
213
+ const { title: _t, cwd: _c, ...rest } = s;
214
+ return rest;
215
+ }
216
+ /** A random id for this machine, kept in the scrub's state folder. */
217
+ function machineId(dir) {
218
+ const file = path.join(dir, "machine-id");
219
+ try {
220
+ const id = fs.readFileSync(file, "utf8").trim();
221
+ if (/^[0-9a-f-]{36}$/.test(id))
222
+ return id;
223
+ }
224
+ catch { /* first run */ }
225
+ const id = crypto.randomUUID();
226
+ try {
227
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
228
+ fs.writeFileSync(file, id, { mode: 0o600 });
229
+ }
230
+ catch { /* fall back to a per-run id */ }
231
+ return id;
232
+ }
233
+ const MAX_SESSIONS = 200;
234
+ function buildReport(input) {
235
+ const s = input.summary;
236
+ return {
237
+ machine: { id: input.machineId, name: os.hostname().slice(0, 80) },
238
+ at: input.at,
239
+ complete: input.complete,
240
+ scanned: s.scanned,
241
+ cleaned: s.cleaned,
242
+ redactions: s.redactions,
243
+ errors: s.errors,
244
+ skippedRecent: s.skippedRecent,
245
+ skippedLarge: s.skippedLarge,
246
+ byHarness: Object.entries(s.byHarness).map(([harness, t]) => ({ harness, files: t.files, redactions: t.redactions })),
247
+ leftInCursor: s.leftInCursor,
248
+ sessions: input.sessions
249
+ .slice(0, MAX_SESSIONS)
250
+ .map((d) => redactExcluded(d, input.ignoredPaths))
251
+ .map(({ cwd: _cwd, ...rest }) => rest),
252
+ };
253
+ }
254
+ /** Best-effort POST to Codiedev. Never throws. */
255
+ async function sendReport(backendUrl, token, payload, cliVersion, fetchImpl = fetch) {
256
+ try {
257
+ const res = await fetchImpl(`${backendUrl}/api/cli/localScrub`, {
258
+ method: "POST",
259
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, "X-Codiedev-Cli-Version": cliVersion },
260
+ body: JSON.stringify(payload),
261
+ signal: AbortSignal.timeout(15_000),
262
+ });
263
+ return res.ok;
264
+ }
265
+ catch {
266
+ return false;
267
+ }
268
+ }
package/dist/managedPi.js CHANGED
@@ -94,7 +94,8 @@ exports.BLOCKED_BASE_URL = "http://127.0.0.1:9/blocked-by-codiedev";
94
94
  * joins the captured pi session (same headers as the OpenCode metadata
95
95
  * plugin, assets/git-refs.mjs). */
96
96
  exports.GUARD_SOURCE = `// Written by Codiedev on every \`codie --pi\` launch. Edits are overwritten.
97
- import { execFileSync } from "node:child_process";
97
+ import { execFile, execFileSync } from "node:child_process";
98
+ import { readFileSync } from "node:fs";
98
99
  import { getProviders } from "@mariozechner/pi-ai";
99
100
 
100
101
  const PROVIDER = ${JSON.stringify(exports.PI_PROVIDER_ID)};
@@ -153,8 +154,23 @@ export default function (pi) {
153
154
  const p = String(event?.input?.path ?? "").replace(/\\\\/g, "/").match(/\\/([a-z0-9][a-z0-9_.:-]{0,119})\\/SKILL\\.md$/i);
154
155
  if (event?.toolName === "read" && !event.isError && p && skills.size < 50) skills.add(p[1]);
155
156
  });
157
+ // Browser sign-in: pi reads CODIEDEV_GATEWAY_KEY at request time, so put
158
+ // the current token there, refreshing it as it nears expiry (same logic as
159
+ // assets/session-key.mjs). Pasted API tokens: no variables, nothing changes.
160
+ const freshKey = async () => {
161
+ const env = process.env;
162
+ if (!env.CODIEDEV_SESSION_CONFIG || !env.CODIEDEV_CLI_JS || !env.CODIEDEV_NODE) return;
163
+ const read = () => { try { const c = JSON.parse(readFileSync(env.CODIEDEV_SESSION_CONFIG, "utf8")); return c && c.auth && c.token ? c : null; } catch { return null; } };
164
+ let s = read();
165
+ if (!s) return;
166
+ if (!(Number(s.auth.accessTokenExpiresAt) - Date.now() > 30 * 60000)) {
167
+ await new Promise((r) => execFile(env.CODIEDEV_NODE, [env.CODIEDEV_CLI_JS, "session-refresh"], { timeout: 30000 }, () => r()));
168
+ s = read() ?? s;
169
+ }
170
+ env.CODIEDEV_GATEWAY_KEY = s.token;
171
+ };
156
172
  // Before every model call in the turn: the agent may have switched branch.
157
- pi.on("turn_start", async (_event, ctx) => tag(ctx));
173
+ pi.on("turn_start", async (_event, ctx) => { await freshKey(); tag(ctx); });
158
174
  pi.on("before_provider_request", (_event, ctx) => {
159
175
  // Belt and braces: the request is already headed for BLOCKED.
160
176
  if (!ours(ctx.model)) ctx.abort();
package/dist/mcp.js CHANGED
@@ -51,6 +51,7 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
51
51
  const fs = __importStar(require("fs"));
52
52
  const path = __importStar(require("path"));
53
53
  const utils_1 = require("./utils");
54
+ const cliSession_1 = require("./cliSession");
54
55
  const shared_1 = require("./commands/shared");
55
56
  const inboxFormat_1 = require("./inboxFormat");
56
57
  const PKG_NAME = "codiedev";
@@ -399,6 +400,8 @@ function emitTelemetry(config, event) {
399
400
  });
400
401
  }
401
402
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
403
+ // Long-lived process: the access token may expire while it runs.
404
+ await (0, cliSession_1.ensureFreshToken)({ configPath: utils_1.CONFIG_PATH }).catch(() => undefined);
402
405
  const config = (0, utils_1.readConfig)();
403
406
  if (!config) {
404
407
  return {
@@ -0,0 +1,4 @@
1
+ import { type ExtraRule, type ScrubResult } from "./rules";
2
+ export declare function setExtraRules(rules: ExtraRule[]): void;
3
+ /** scrubSecrets with this run's extra rules. */
4
+ export declare function scrub(text: string): ScrubResult;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setExtraRules = setExtraRules;
4
+ exports.scrub = scrub;
5
+ // Rules added for this run of the laptop scrub on top of the built-in ones:
6
+ // the company's own token formats (from the server) and the exact values of
7
+ // this person's own secrets (knownSecrets.ts). Set once by the detached
8
+ // scrub child before it runs; empty everywhere else.
9
+ const rules_1 = require("./rules");
10
+ let extra = [];
11
+ function setExtraRules(rules) {
12
+ extra = rules;
13
+ }
14
+ /** scrubSecrets with this run's extra rules. */
15
+ function scrub(text) {
16
+ return extra.length ? (0, rules_1.scrubSecrets)(text, { extraRules: extra }) : (0, rules_1.scrubSecrets)(text);
17
+ }
@@ -0,0 +1,86 @@
1
+ export interface Redaction {
2
+ rule: string;
3
+ start: number;
4
+ end: number;
5
+ }
6
+ export interface ScrubResult {
7
+ text: string;
8
+ redactions: Redaction[];
9
+ }
10
+ /** Rules a caller adds for one scrub (a company's own token formats). */
11
+ export interface ExtraRule {
12
+ name: string;
13
+ re: RegExp;
14
+ keywords?: readonly string[];
15
+ }
16
+ /** Shannon entropy in bits per character, as Gitleaks measures it. */
17
+ export declare function shannonEntropy(s: string): number;
18
+ export declare function scrubSecrets(input: string, opts?: {
19
+ extraRules?: readonly ExtraRule[];
20
+ }): ScrubResult;
21
+ /** Rewrite `input` with each span replaced by `[REDACTED:<rule>]`. Spans must
22
+ * be sorted by start and not overlap. One forward pass: repeated
23
+ * slice+concat would copy the whole string per redaction (quadratic). */
24
+ export declare function redactSpans(input: string, spans: readonly Redaction[]): string;
25
+ /** Where in a session a secret appeared. */
26
+ export type SecretWhere = "prompt" | "tool_call" | "commit" | "tool_output" | "assistant" | "unknown";
27
+ export interface SecretLocation {
28
+ where: SecretWhere;
29
+ /** Tool or MCP tool name for tool calls/outputs when the transcript has it. */
30
+ tool?: string;
31
+ }
32
+ export interface SecretFinding {
33
+ rule: string;
34
+ /** Last four alphanumeric characters of the redacted value — enough for a
35
+ * person to recognise which key to rotate, never enough to use it. */
36
+ hint: string;
37
+ /** Distinct places in the session the key appeared. */
38
+ locations: SecretLocation[];
39
+ /** The variable or field it was assigned to (`X_BEARER_TOKEN`), when the
40
+ * text shows one. A name, never the value. */
41
+ name?: string;
42
+ /** The file it was written to or read from (`altcap-bot/.env`), when the
43
+ * text shows one: last two path parts only. */
44
+ file?: string;
45
+ }
46
+ /** What a caller may check before a key becomes a finding (exceptions).
47
+ * `value` is the key itself: use it to compare, never store it. */
48
+ export interface KeyContext {
49
+ rule: string;
50
+ value: string;
51
+ name?: string;
52
+ file?: string;
53
+ }
54
+ export declare const GENERIC_RULES: ReadonlySet<string>;
55
+ /** Values that share a key's shape but are test or sample values: words
56
+ * joined by _ or - with only digits or one repeated character between
57
+ * (`cdv_local_pilot_token_2026`, `…_token_000000`), a long counting run
58
+ * (`sk-1234567890…`), or a long repeated tail. Still removed from sessions;
59
+ * just not listed as a key to rotate. */
60
+ export declare function looksLikeTestValue(value: string): boolean;
61
+ /** One finding per distinct (rule, last-4) in a scrub result, with every
62
+ * distinct place it appeared. */
63
+ export declare function secretFindings(input: string, redactions: Redaction[], opts?: {
64
+ skip?: (k: KeyContext) => boolean;
65
+ }): SecretFinding[];
66
+ export interface PossibleSecret {
67
+ label: string;
68
+ prefix?: string;
69
+ hint: string;
70
+ length: number;
71
+ shape: "hex" | "base64" | "alnum";
72
+ snippet: string;
73
+ locations: SecretLocation[];
74
+ /** Spans in the text this was read from, for redaction once confirmed. */
75
+ spans: Array<{
76
+ start: number;
77
+ end: number;
78
+ }>;
79
+ }
80
+ export declare function possibleSecrets(text: string, limit?: number): PossibleSecret[];
81
+ /** Redact every occurrence of an already-reviewed possible secret. */
82
+ export declare function redactPossible(text: string, match: {
83
+ label: string;
84
+ hint: string;
85
+ length: number;
86
+ }, rule?: string): ScrubResult;