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,43 @@
1
+ import { type ExtraRule } from "./scrub/rules";
2
+ export interface KnownSecret {
3
+ value: string;
4
+ name?: string;
5
+ /** Where it came from: last two path parts, e.g. `api/.env`. */
6
+ file: string;
7
+ }
8
+ /** A value worth matching: long and random enough that it can't be an
9
+ * ordinary word, path or setting, and safe to fingerprint (a short or
10
+ * low-entropy value could be guessed back from its hash). */
11
+ export declare function worthMatching(v: string): boolean;
12
+ /** KEY=value lines (.env, .ini-style credentials, .npmrc, .pypirc). */
13
+ export declare function parseAssignments(text: string): Array<{
14
+ name: string;
15
+ value: string;
16
+ }>;
17
+ /** Per-user credential files, and .env files in the repos sessions ran in. */
18
+ /** Per-user credential files read, relative to home. Listed in the product
19
+ * (Team Security → Settings) and in `codiedev scrub --help`. */
20
+ export declare const CREDENTIAL_FILES: readonly [".aws/credentials", ".npmrc", ".pypirc", ".netrc", ".git-credentials", ".config/gh/hosts.yml", ".docker/config.json", ".config/hub", ".gem/credentials", ".config/openai/credentials"];
21
+ export declare function credentialFiles(home: string, cwds: readonly string[]): string[];
22
+ export declare function collectKnownSecrets(home?: string, cwds?: readonly string[]): KnownSecret[];
23
+ /** One scrub rule matching any of these exact values (and their JSON-escaped
24
+ * forms), on non-alphanumeric boundaries. */
25
+ export declare function knownSecretRule(secrets: readonly KnownSecret[]): ExtraRule | null;
26
+ /** Same as the server's knownHash: HMAC-SHA256 with the company's key (hex,
27
+ * from the scrub policy), first 32 hex. */
28
+ export declare function knownHash(keyHex: string, value: string): string;
29
+ export declare const hintOf: (value: string) => string;
30
+ export declare function fingerprints(keyHex: string, secrets: readonly KnownSecret[]): {
31
+ file: string;
32
+ name?: string | undefined;
33
+ hash: string;
34
+ hint: string;
35
+ }[];
36
+ /** Send keyed fingerprints (never values). Replaces this person's set.
37
+ * Only in "server" mode, with the key the server sent. Best-effort. */
38
+ export declare function uploadKnownSecrets(config: {
39
+ backendUrl?: string;
40
+ token?: string;
41
+ } | null, keyHex: string, secrets: readonly KnownSecret[], fetchImpl?: typeof fetch): Promise<boolean>;
42
+ export declare function rememberCwd(stateDir: string, cwd: string | undefined): void;
43
+ export declare function recentCwds(stateDir: string): string[];
@@ -0,0 +1,241 @@
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.hintOf = exports.CREDENTIAL_FILES = void 0;
37
+ exports.worthMatching = worthMatching;
38
+ exports.parseAssignments = parseAssignments;
39
+ exports.credentialFiles = credentialFiles;
40
+ exports.collectKnownSecrets = collectKnownSecrets;
41
+ exports.knownSecretRule = knownSecretRule;
42
+ exports.knownHash = knownHash;
43
+ exports.fingerprints = fingerprints;
44
+ exports.uploadKnownSecrets = uploadKnownSecrets;
45
+ exports.rememberCwd = rememberCwd;
46
+ exports.recentCwds = recentCwds;
47
+ // This person's own secrets: values in their .env and credentials files.
48
+ // The laptop scrub removes those exact values from past sessions on this
49
+ // machine, in any format. Only where the laptop scrub is on, and only as the
50
+ // company's admin set it (Team Security → Settings):
51
+ // off these files are never read;
52
+ // laptop read and matched here; nothing about them leaves (default);
53
+ // server also sends a keyed fingerprint of each (HMAC with a per-company
54
+ // key from the server, never the value) so uploaded sessions are
55
+ // checked too.
56
+ // Files read: CREDENTIAL_FILES below, and .env / .env.* (not .example,
57
+ // .sample, .template) in the repos sessions started in.
58
+ const crypto = __importStar(require("crypto"));
59
+ const fs = __importStar(require("fs"));
60
+ const os = __importStar(require("os"));
61
+ const path = __importStar(require("path"));
62
+ const rules_1 = require("./scrub/rules");
63
+ const MAX_SECRETS = 500;
64
+ const MAX_FILE_BYTES = 256 * 1024;
65
+ const MAX_CWDS = 30;
66
+ /** A value worth matching: long and random enough that it can't be an
67
+ * ordinary word, path or setting, and safe to fingerprint (a short or
68
+ * low-entropy value could be guessed back from its hash). */
69
+ function worthMatching(v) {
70
+ if (v.length < 16 || v.length > 512)
71
+ return false;
72
+ // URLs aren't the secret; a password inside one is caught by url-password.
73
+ if (/\s/.test(v) || /^[a-z][a-z0-9+.-]*:\/\//i.test(v))
74
+ return false;
75
+ if (/^(?:true|false|null|undefined|localhost)$/i.test(v) || /^[\d.]+$/.test(v))
76
+ return false;
77
+ if (/^[~./]/.test(v) || /\$\{|\$\(/.test(v))
78
+ return false;
79
+ if ((0, rules_1.looksLikeTestValue)(v))
80
+ return false;
81
+ return (0, rules_1.shannonEntropy)(v) >= 3.5;
82
+ }
83
+ const unquote = (v) => v.trim().replace(/^(['"])(.*)\1$/, "$2");
84
+ const short = (p) => p.split(path.sep).filter(Boolean).slice(-2).join("/");
85
+ /** KEY=value lines (.env, .ini-style credentials, .npmrc, .pypirc). */
86
+ function parseAssignments(text) {
87
+ const out = [];
88
+ for (const line of text.split(/\r?\n/)) {
89
+ const m = line.match(/^\s*(?:export\s+)?([A-Za-z_./:@-][\w./:@-]*)\s*[=:]\s*(.+?)\s*$/);
90
+ if (!m || line.trim().startsWith("#") || line.trim().startsWith(";"))
91
+ continue;
92
+ out.push({ name: m[1].replace(/^\/\/[^:]*:/, ""), value: unquote(m[2].replace(/\s+#.*$/, "")) });
93
+ }
94
+ return out;
95
+ }
96
+ function read(file) {
97
+ try {
98
+ const st = fs.statSync(file);
99
+ if (!st.isFile() || st.size > MAX_FILE_BYTES)
100
+ return null;
101
+ return fs.readFileSync(file, "utf8");
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ /** Per-user credential files, and .env files in the repos sessions ran in. */
108
+ /** Per-user credential files read, relative to home. Listed in the product
109
+ * (Team Security → Settings) and in `codiedev scrub --help`. */
110
+ exports.CREDENTIAL_FILES = [
111
+ ".aws/credentials", ".npmrc", ".pypirc", ".netrc", ".git-credentials", ".config/gh/hosts.yml",
112
+ ".docker/config.json", ".config/hub", ".gem/credentials", ".config/openai/credentials",
113
+ ];
114
+ function credentialFiles(home, cwds) {
115
+ const global = exports.CREDENTIAL_FILES.map((p) => path.join(home, p));
116
+ const envs = [];
117
+ for (const cwd of cwds) {
118
+ let entries = [];
119
+ try {
120
+ entries = fs.readdirSync(cwd);
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ for (const e of entries) {
126
+ if (/^\.env(?:\.[\w-]+)?$/.test(e) && !/\.(?:example|sample|template|dist|defaults?)$/i.test(e))
127
+ envs.push(path.join(cwd, e));
128
+ }
129
+ }
130
+ return [...global, ...envs];
131
+ }
132
+ function collectKnownSecrets(home = os.homedir(), cwds = []) {
133
+ const seen = new Map();
134
+ const add = (value, file, name) => {
135
+ if (seen.size >= MAX_SECRETS || !worthMatching(value) || seen.has(value))
136
+ return;
137
+ seen.set(value, { value, file: short(file), ...(name ? { name: name.slice(0, 64) } : {}) });
138
+ };
139
+ for (const file of credentialFiles(home, cwds)) {
140
+ const text = read(file);
141
+ if (!text)
142
+ continue;
143
+ const base = path.basename(file);
144
+ if (base === "config.json") {
145
+ try {
146
+ const auths = JSON.parse(text).auths ?? {};
147
+ for (const [host, a] of Object.entries(auths)) {
148
+ if (a.auth)
149
+ add(a.auth, file, `docker ${host}`);
150
+ if (a.identitytoken)
151
+ add(a.identitytoken, file, `docker ${host}`);
152
+ }
153
+ }
154
+ catch { /* not JSON */ }
155
+ continue;
156
+ }
157
+ if (base === ".git-credentials") {
158
+ for (const m of text.matchAll(/^https?:\/\/[^:\s]+:([^@\s]+)@(\S+)$/gm))
159
+ add(decodeURIComponent(m[1]), file, `git ${m[2]}`);
160
+ continue;
161
+ }
162
+ if (base === ".netrc") {
163
+ for (const m of text.matchAll(/\bpassword\s+(\S+)/g))
164
+ add(m[1], file, "netrc password");
165
+ continue;
166
+ }
167
+ for (const { name, value } of parseAssignments(text))
168
+ add(value, file, name);
169
+ }
170
+ return [...seen.values()];
171
+ }
172
+ /** One scrub rule matching any of these exact values (and their JSON-escaped
173
+ * forms), on non-alphanumeric boundaries. */
174
+ function knownSecretRule(secrets) {
175
+ if (secrets.length === 0)
176
+ return null;
177
+ const forms = new Set();
178
+ for (const s of secrets) {
179
+ forms.add(s.value);
180
+ const escaped = JSON.stringify(s.value).slice(1, -1);
181
+ if (escaped !== s.value)
182
+ forms.add(escaped);
183
+ }
184
+ const alt = [...forms].sort((a, b) => b.length - a.length).map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
185
+ return { name: "known-secret", re: new RegExp(`(?<![A-Za-z0-9])(?:${alt})(?![A-Za-z0-9])`, "g") };
186
+ }
187
+ /** Same as the server's knownHash: HMAC-SHA256 with the company's key (hex,
188
+ * from the scrub policy), first 32 hex. */
189
+ function knownHash(keyHex, value) {
190
+ return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(value).digest("hex").slice(0, 32);
191
+ }
192
+ const hintOf = (value) => value.replace(/[^A-Za-z0-9]/g, "").slice(-4);
193
+ exports.hintOf = hintOf;
194
+ function fingerprints(keyHex, secrets) {
195
+ return secrets.map((s) => ({ hash: knownHash(keyHex, s.value), hint: (0, exports.hintOf)(s.value), ...(s.name ? { name: s.name } : {}), file: s.file }));
196
+ }
197
+ /** Send keyed fingerprints (never values). Replaces this person's set.
198
+ * Only in "server" mode, with the key the server sent. Best-effort. */
199
+ async function uploadKnownSecrets(config, keyHex, secrets, fetchImpl = fetch) {
200
+ if (!config?.backendUrl || !config.token || !/^[0-9a-f]{64}$/.test(keyHex))
201
+ return false;
202
+ try {
203
+ const res = await fetchImpl(`${config.backendUrl}/api/cli/knownSecrets`, {
204
+ method: "POST",
205
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.token}` },
206
+ body: JSON.stringify({ secrets: fingerprints(keyHex, secrets) }),
207
+ signal: AbortSignal.timeout(15_000),
208
+ });
209
+ return res.ok;
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ }
215
+ // Repos sessions start in, so the scrub knows which .env files are this
216
+ // person's. Kept next to the scrub state; the last 30, newest first.
217
+ function rememberCwd(stateDir, cwd) {
218
+ if (!cwd || !path.isAbsolute(cwd))
219
+ return;
220
+ try {
221
+ const file = path.join(stateDir, "cwds.json");
222
+ let list = [];
223
+ try {
224
+ list = JSON.parse(fs.readFileSync(file, "utf8"));
225
+ }
226
+ catch { /* first */ }
227
+ list = [cwd, ...list.filter((c) => c !== cwd)].slice(0, MAX_CWDS);
228
+ fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
229
+ fs.writeFileSync(file, JSON.stringify(list), { mode: 0o600 });
230
+ }
231
+ catch { /* best-effort */ }
232
+ }
233
+ function recentCwds(stateDir) {
234
+ try {
235
+ const list = JSON.parse(fs.readFileSync(path.join(stateDir, "cwds.json"), "utf8"));
236
+ return Array.isArray(list) ? list.filter((c) => typeof c === "string") : [];
237
+ }
238
+ catch {
239
+ return [];
240
+ }
241
+ }
@@ -8,13 +8,18 @@ export declare function launcherPath(): string;
8
8
  /** True iff `cmd` invokes the stable launcher (from any state dir). */
9
9
  export declare function usesLauncher(cmd: string | undefined): boolean;
10
10
  export declare function readInstallRecord(): InstallRecord | null;
11
+ /**
12
+ * The hook script worth recording: absolute and present, and a global install over an npx cache
13
+ * copy (npm prunes ~/.npm/_npx, and hooks pointed there go quietly dead). Null = don't record.
14
+ */
15
+ export declare function stableHookScript(hookScript: string, globalCandidates?: string[]): string | null;
11
16
  /** Point the launcher at `hookScript`. Atomic, so a hook mid-launch never reads half a file. */
12
- export declare function recordCurrentInstall(hookScript: string): boolean;
17
+ export declare function recordCurrentInstall(requested: string): boolean;
13
18
  /**
14
19
  * Make sure the launcher exists and forwards to `hookScript`, returning the
15
20
  * launcher path to bake into hook configs — or null if the state dir is not
16
21
  * writable, in which case callers keep baking the direct path. Memoized:
17
22
  * one connect writes ~20 hook entries.
18
23
  */
19
- export declare function ensureLauncher(hookScript: string): string | null;
24
+ export declare function ensureLauncher(requested: string): string | null;
20
25
  export declare function resetLauncherCacheForTests(): void;
package/dist/launcher.js CHANGED
@@ -48,6 +48,7 @@ exports.LAUNCHER_FILE = void 0;
48
48
  exports.launcherPath = launcherPath;
49
49
  exports.usesLauncher = usesLauncher;
50
50
  exports.readInstallRecord = readInstallRecord;
51
+ exports.stableHookScript = stableHookScript;
51
52
  exports.recordCurrentInstall = recordCurrentInstall;
52
53
  exports.ensureLauncher = ensureLauncher;
53
54
  exports.resetLauncherCacheForTests = resetLauncherCacheForTests;
@@ -114,8 +115,35 @@ function packageVersionFor(hookScript) {
114
115
  return undefined;
115
116
  }
116
117
  }
118
+ /**
119
+ * The hook script worth recording: absolute and present, and a global install over an npx cache
120
+ * copy (npm prunes ~/.npm/_npx, and hooks pointed there go quietly dead). Null = don't record.
121
+ */
122
+ function stableHookScript(hookScript, globalCandidates = globalHookScripts()) {
123
+ if (!hookScript || !path.isAbsolute(hookScript) || hookScript.includes(`${path.sep}~${path.sep}`))
124
+ return null;
125
+ if (!fs.existsSync(hookScript))
126
+ return null;
127
+ if (hookScript.includes(`${path.sep}_npx${path.sep}`)) {
128
+ const global = globalCandidates.find((p) => path.isAbsolute(p) && fs.existsSync(p));
129
+ if (global)
130
+ return global;
131
+ }
132
+ return hookScript;
133
+ }
134
+ /** Where a global `npm i -g codiedev` puts hook.js, next to the node running this. */
135
+ function globalHookScripts() {
136
+ const prefix = path.dirname(path.dirname(process.execPath));
137
+ return [
138
+ path.join(prefix, "lib", "node_modules", "codiedev", "dist", "hook.js"),
139
+ path.join(prefix, "node_modules", "codiedev", "dist", "hook.js"),
140
+ ];
141
+ }
117
142
  /** Point the launcher at `hookScript`. Atomic, so a hook mid-launch never reads half a file. */
118
- function recordCurrentInstall(hookScript) {
143
+ function recordCurrentInstall(requested) {
144
+ const hookScript = stableHookScript(requested);
145
+ if (!hookScript)
146
+ return false;
119
147
  try {
120
148
  const record = {
121
149
  hookScript,
@@ -136,11 +164,17 @@ let ensuredFor;
136
164
  * writable, in which case callers keep baking the direct path. Memoized:
137
165
  * one connect writes ~20 hook entries.
138
166
  */
139
- function ensureLauncher(hookScript) {
167
+ function ensureLauncher(requested) {
168
+ const hookScript = stableHookScript(requested);
169
+ if (!hookScript)
170
+ return null;
140
171
  if (ensuredFor !== undefined && ensuredFor === hookScript)
141
172
  return launcherPath();
142
173
  try {
143
174
  const target = launcherPath();
175
+ // A relative launcher path in a hook config only works from one folder; never bake one.
176
+ if (!path.isAbsolute(target))
177
+ return null;
144
178
  const source = launcherSource(hookScript);
145
179
  let current = null;
146
180
  try {
@@ -0,0 +1,172 @@
1
+ import { spawn as nodeSpawn } from "child_process";
2
+ import type { ExtraRule } from "./scrub/rules";
3
+ import { type KeyHit, type SessionDetection } from "./localScrubReport";
4
+ export type Harness = "claude-code" | "codex" | "cursor" | "copilot" | "opencode" | "pi";
5
+ export type FileKind = "jsonl" | "json" | "text" | "yaml";
6
+ /** cursor-*-db stores are only counted, never written: see countCursorDb. */
7
+ export type StoreKind = FileKind | "opencode-db" | "cursor-agent-db" | "cursor-ide-db";
8
+ export interface Store {
9
+ harness: Harness;
10
+ kind: StoreKind;
11
+ /** A file, or a directory searched recursively. */
12
+ root: string;
13
+ /** For directory roots: which files belong to this store. */
14
+ match?: (file: string) => boolean;
15
+ }
16
+ export declare const MIN_AGE_MS: number;
17
+ export declare const RUN_INTERVAL_MS: number;
18
+ /** Every place a harness keeps conversation text on this machine. Snapshots
19
+ * of the user's own files (Copilot rewind, Claude file-history) are left
20
+ * alone: cleaning them would change code the user restores later. */
21
+ export declare function sessionStores(env?: NodeJS.ProcessEnv, home?: string, stateDir?: string): Store[];
22
+ /** The Cursor IDE's chat database (VS Code layout, per platform). */
23
+ export declare function cursorIdeDb(env?: NodeJS.ProcessEnv, home?: string, platform?: NodeJS.Platform): string;
24
+ export interface CleanResult {
25
+ text: string;
26
+ redactions: number;
27
+ }
28
+ /** One JSON document. Cleans the raw text (keeps formatting byte-for-byte
29
+ * outside the keys); if that would break the JSON, cleans value by value. */
30
+ export declare function cleanJson(text: string): CleanResult;
31
+ /** JSONL: each line is its own JSON document. Line endings are kept. */
32
+ export declare function cleanJsonl(text: string): CleanResult;
33
+ export declare function cleanText(text: string): CleanResult;
34
+ /** Flat `key: value` YAML. A redaction at the start of an unquoted value
35
+ * would read as a YAML list (`[REDACTED:…]`), so such values get quoted. */
36
+ export declare function cleanYaml(text: string): CleanResult;
37
+ export declare function cleanFor(kind: FileKind, text: string): CleanResult;
38
+ export type FileOutcome = "cleaned" | "would-clean" | "clean" | "too-recent" | "too-large" | "changed-while-cleaning" | "error";
39
+ export interface FileDeps {
40
+ now: number;
41
+ minAgeMs: number;
42
+ dryRun: boolean;
43
+ /** OpenCode database: only these session ids (tests, `--session`). */
44
+ onlySessions?: Set<string>;
45
+ /** Test seam: runs after the cleaned copy is written, before the swap. */
46
+ beforeSwap?: (file: string) => void;
47
+ }
48
+ export interface FileDetail {
49
+ keys: KeyHit[];
50
+ title?: string;
51
+ cwd?: string;
52
+ }
53
+ /** Clean one file in place, safely. Returns the outcome, how many keys, and
54
+ * (when keys were found) which ones plus the session's title and folder. */
55
+ export declare function cleanFile(file: string, kind: FileKind, deps: FileDeps): {
56
+ outcome: FileOutcome;
57
+ redactions: number;
58
+ detail?: FileDetail;
59
+ };
60
+ export declare function filesOf(store: Store): string[];
61
+ /** OpenCode keeps messages and parts as JSON text rows. Clean rows whose
62
+ * session hasn't changed in the last hour, inside one transaction; SQLite's
63
+ * own locking keeps a running OpenCode safe (we wait or skip, never corrupt). */
64
+ export interface DbResult {
65
+ outcome: FileOutcome | "no-sqlite";
66
+ redactions: number;
67
+ rows: number;
68
+ /** Per session: keys found, title and folder. */
69
+ sessions?: Map<string, FileDetail>;
70
+ }
71
+ export declare function cleanOpenCodeDb(file: string, deps: FileDeps): DbResult;
72
+ /** Keys in a Cursor chat database, read-only. cursor-agent's store.db is
73
+ * content-addressed (each blob's id is the sha256 of its bytes and blobs link
74
+ * to each other by id), so editing one key would break the chat. The IDE's
75
+ * state.vscdb holds chats as JSON under bubbleId:/composerData: keys; it is
76
+ * counted too until it can be tested against a live Cursor. */
77
+ export interface CursorCount {
78
+ keys: number;
79
+ chats: number;
80
+ details: SessionDetection[];
81
+ }
82
+ export declare function countCursorDb(file: string, kind: "cursor-agent-db" | "cursor-ide-db"): CursorCount | null;
83
+ export interface RunOptions {
84
+ now?: number;
85
+ minAgeMs?: number;
86
+ dryRun?: boolean;
87
+ budgetMs?: number;
88
+ /** Restrict the run to these files (tests, `codiedev scrub --path`). */
89
+ only?: string[];
90
+ /** Restrict the OpenCode database to these sessions (`--session`). */
91
+ onlySessions?: string[];
92
+ stores?: Store[];
93
+ stateDir?: string;
94
+ }
95
+ export interface RunSummary {
96
+ dryRun: boolean;
97
+ scanned: number;
98
+ cleaned: number;
99
+ redactions: number;
100
+ skippedRecent: number;
101
+ skippedLarge: number;
102
+ changedWhileCleaning: number;
103
+ errors: number;
104
+ unfinished: boolean;
105
+ byHarness: Partial<Record<Harness, {
106
+ files: number;
107
+ redactions: number;
108
+ }>>;
109
+ /** Keys still in Cursor's own chat stores, which are never changed. */
110
+ leftInCursor: {
111
+ keys: number;
112
+ chats: number;
113
+ };
114
+ /** Every session keys were found in this run: what, and removed or not. */
115
+ sessions: SessionDetection[];
116
+ }
117
+ export declare function runLocalScrub(opts?: RunOptions): RunSummary;
118
+ /** Run the scrub and send the audit to Codiedev when connected. Dry runs and
119
+ * runs limited by --path/--session are never sent. */
120
+ export declare function runAndReport(opts?: RunOptions): Promise<RunSummary & {
121
+ reported: boolean;
122
+ }>;
123
+ /** Should this person's machine run the background scrub? Off by default:
124
+ * only when they turned it on in Security → Settings, or their company
125
+ * requires it. Any failure (offline, older server, not signed in) means no. */
126
+ type ScrubConfig = {
127
+ token?: string;
128
+ backendUrl?: string;
129
+ companyId?: string;
130
+ } | null;
131
+ /** A company token format as the server sends it: a validated, bounded
132
+ * pattern source (prefix + class + length), never free-form. */
133
+ export interface TokenFormatRule {
134
+ name: string;
135
+ source: string;
136
+ keyword?: string;
137
+ }
138
+ export type OwnSecretsMode = "off" | "laptop" | "server";
139
+ export interface ScrubPolicy {
140
+ enabled: boolean;
141
+ formats: TokenFormatRule[];
142
+ ownSecrets: OwnSecretsMode;
143
+ fingerprintKey?: string;
144
+ }
145
+ export declare function scrubPolicy(fetchImpl?: typeof fetch, getConfig?: () => ScrubConfig): Promise<ScrubPolicy>;
146
+ /** Should this person's machine run the background scrub? */
147
+ export declare function scrubAllowed(fetchImpl?: typeof fetch, getConfig?: () => ScrubConfig): Promise<boolean>;
148
+ /** Turn the company's token formats into scrub rules; a bad one is skipped. */
149
+ export declare function formatRules(formats: readonly TokenFormatRule[]): ExtraRule[];
150
+ /** The scheduled run: policy check, then this person's own secrets (as the
151
+ * admin set it: not read / matched here / also keyed fingerprints to the
152
+ * server) and the company's formats, then the scrub and its report. */
153
+ export declare function runScheduledScrub(opts?: {
154
+ fetchImpl?: typeof fetch;
155
+ getConfig?: () => ScrubConfig;
156
+ home?: string;
157
+ stateDir?: string;
158
+ }): Promise<boolean>;
159
+ export interface SpawnOptions {
160
+ connected?: boolean;
161
+ /** The session's working directory (its repo). */
162
+ cwd?: string;
163
+ now?: number;
164
+ stateDir?: string;
165
+ env?: NodeJS.ProcessEnv;
166
+ hookEntry?: string;
167
+ spawn?: typeof nodeSpawn;
168
+ }
169
+ /** Start a background scrub if the last one was over an hour ago. Stamps
170
+ * first so sessions opening at the same time don't each start one. */
171
+ export declare function maybeSpawnLocalScrub(opts?: SpawnOptions): "spawned" | "recent" | "disabled" | "error";
172
+ export {};