@dashclaw/cli 0.7.6 → 0.8.1
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/README.md +0 -61
- package/bin/dashclaw.js +2 -770
- package/lib/claude/install.js +411 -412
- package/lib/codex/install.js +10 -9
- package/lib/up/index.js +45 -4
- package/package.json +37 -37
- package/lib/code/apply.js +0 -164
- package/lib/code/codex-parser.vendored.js +0 -360
- package/lib/code/ingest-codex.js +0 -244
- package/lib/code/ingest.js +0 -261
- package/lib/code/memo.js +0 -57
- package/lib/code/vendored.js +0 -219
- package/lib/cost.js +0 -108
- package/lib/env.js +0 -69
- package/lib/posture.js +0 -45
package/lib/codex/install.js
CHANGED
|
@@ -59,12 +59,10 @@ const HOOK_FILES = [
|
|
|
59
59
|
'dashclaw_pretool.py',
|
|
60
60
|
'dashclaw_posttool.py',
|
|
61
61
|
'dashclaw_stop.py',
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// SessionStart hook event was confirmed to fire (see PLUGIN_PARITY.md).
|
|
67
|
-
'dashclaw_session_digest.py',
|
|
62
|
+
// SessionStart enforcement-liveness probe (v8.2) — the SessionStart hook
|
|
63
|
+
// (wired once codex-cli 0.139.0's SessionStart event was confirmed to fire;
|
|
64
|
+
// see PLUGIN_PARITY.md). Replaced the retired session-digest hook.
|
|
65
|
+
'enforcement_liveness_probe.py',
|
|
68
66
|
];
|
|
69
67
|
const HOOK_INTEL_DIR = 'dashclaw_agent_intel';
|
|
70
68
|
|
|
@@ -192,7 +190,7 @@ export function buildConfigTomlBlock({
|
|
|
192
190
|
const pre = join(hooksDir, 'dashclaw_pretool.py');
|
|
193
191
|
const post = join(hooksDir, 'dashclaw_posttool.py');
|
|
194
192
|
const stop = join(hooksDir, 'dashclaw_stop.py');
|
|
195
|
-
const sessionStart = join(hooksDir, '
|
|
193
|
+
const sessionStart = join(hooksDir, 'enforcement_liveness_probe.py');
|
|
196
194
|
|
|
197
195
|
const lines = [
|
|
198
196
|
MANAGED_START,
|
|
@@ -244,11 +242,14 @@ export function buildConfigTomlBlock({
|
|
|
244
242
|
'',
|
|
245
243
|
// Codex CLI 0.139.0's hook-event enum contains SessionStart alongside
|
|
246
244
|
// Pre/Post/Stop; wired once that lifecycle was verified to fire
|
|
247
|
-
// (roadmap v3.7 item 6 — see PLUGIN_PARITY.md).
|
|
245
|
+
// (roadmap v3.7 item 6 — see PLUGIN_PARITY.md). Runs the enforcement-
|
|
246
|
+
// liveness probe (v8.2): `--source session-start` throttles it to once/12h
|
|
247
|
+
// and detaches, so session start is never delayed. No --agent-id — the
|
|
248
|
+
// probe forces its own synthetic identity internally.
|
|
248
249
|
'[[hooks.SessionStart]]',
|
|
249
250
|
'[[hooks.SessionStart.hooks]]',
|
|
250
251
|
'type = "command"',
|
|
251
|
-
`command = ${tomlString(`${py} ${sessionStart} --
|
|
252
|
+
`command = ${tomlString(`${py} ${sessionStart} --source session-start`)}`,
|
|
252
253
|
MANAGED_END,
|
|
253
254
|
);
|
|
254
255
|
|
package/lib/up/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// without touching the network, Docker, or a real Next server.
|
|
14
14
|
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
16
17
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
18
|
import { homedir } from 'node:os';
|
|
18
19
|
import { join } from 'node:path';
|
|
@@ -111,6 +112,32 @@ export function rewriteEnvDatabaseUrl(appDir, databaseUrl, logger = console) {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Mint a one-time browser sign-in token into the app's .env.local, BEFORE the
|
|
117
|
+
* server starts (Next reads .env.local at boot). The server consumes it on
|
|
118
|
+
* first use (app/api/auth/local); the 15-minute expiry bounds replay. This is
|
|
119
|
+
* what lets `dashclaw up` open a browser that lands already signed in instead
|
|
120
|
+
* of a login form asking for a password the user never saw.
|
|
121
|
+
* Best-effort: returns null (→ fall back to /setup) when .env.local is absent.
|
|
122
|
+
*/
|
|
123
|
+
export function mintLoginToken(appDir, logger = console) {
|
|
124
|
+
try {
|
|
125
|
+
const envPath = join(appDir, '.env.local');
|
|
126
|
+
if (!existsSync(envPath)) return null;
|
|
127
|
+
const token = randomBytes(24).toString('base64url');
|
|
128
|
+
const line = `DASHCLAW_LOGIN_OTT=${token}.${Date.now() + 15 * 60_000}`;
|
|
129
|
+
const src = readFileSync(envPath, 'utf8');
|
|
130
|
+
const out = /^DASHCLAW_LOGIN_OTT=.*$/m.test(src)
|
|
131
|
+
? src.replace(/^DASHCLAW_LOGIN_OTT=.*$/m, line)
|
|
132
|
+
: `${src}${src.endsWith('\n') || src === '' ? '' : '\n'}${line}\n`;
|
|
133
|
+
writeFileSync(envPath, out);
|
|
134
|
+
return token;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
logger.error(`[warn] Could not mint a browser sign-in link: ${e.message} — sign in with the admin password instead.`);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
114
141
|
/** Default process-liveness probe: signal 0 succeeds iff the pid exists. */
|
|
115
142
|
export function defaultProcessAlive(pid) {
|
|
116
143
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
@@ -125,6 +152,7 @@ export function realDeps() {
|
|
|
125
152
|
chooseDbMode,
|
|
126
153
|
provisionDatabase,
|
|
127
154
|
runSetupScript: runSetupScriptReal,
|
|
155
|
+
mintLoginToken,
|
|
128
156
|
buildApp,
|
|
129
157
|
startServer,
|
|
130
158
|
waitForHealth,
|
|
@@ -210,10 +238,11 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
210
238
|
if (out.ok === false) throw new Error(out.error || 'Setup failed.');
|
|
211
239
|
apiKey = out.apiKey;
|
|
212
240
|
inst = saveInstance(baseDir, { apiKey });
|
|
213
|
-
// setup.mjs
|
|
214
|
-
//
|
|
241
|
+
// setup.mjs prints the password to ITS stderr, but runSetupScript pipes
|
|
242
|
+
// (and on success discards) that stream — so this line is the one place
|
|
243
|
+
// the operator ever sees it. It is also saved to <appDir>/.env.local.
|
|
215
244
|
if (out.adminPassword) {
|
|
216
|
-
logger.
|
|
245
|
+
logger.log(`[ok] Dashboard admin password: ${out.adminPassword} (also saved to ${join(appDir, '.env.local')})`);
|
|
217
246
|
}
|
|
218
247
|
inst = checkpoint(baseDir, 'setup_done');
|
|
219
248
|
}
|
|
@@ -242,7 +271,11 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
242
271
|
// Pid alive but health check failed — fall through to a fresh start.
|
|
243
272
|
}
|
|
244
273
|
}
|
|
274
|
+
// One-time browser sign-in: only mintable for a server WE are about to start
|
|
275
|
+
// (Next reads .env.local at boot; a reused server would never see the token).
|
|
276
|
+
let loginToken = null;
|
|
245
277
|
if (!reusedServer) {
|
|
278
|
+
loginToken = deps.mintLoginToken?.(appDir, logger) ?? null;
|
|
246
279
|
child = deps.startServer({ appDir, port, logger });
|
|
247
280
|
inst = saveInstance(baseDir, { pid: child.pid });
|
|
248
281
|
try {
|
|
@@ -271,8 +304,16 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
271
304
|
}
|
|
272
305
|
|
|
273
306
|
// 8. open -----------------------------------------------------------------
|
|
307
|
+
// With a fresh token the browser opens /login?ott=... and lands signed in
|
|
308
|
+
// (redirecting on to /setup); otherwise it opens /setup directly and any
|
|
309
|
+
// protected page will ask for the admin password.
|
|
310
|
+
const signInUrl = loginToken
|
|
311
|
+
? `${baseUrl}/login?ott=${loginToken}&next=${encodeURIComponent('/setup')}`
|
|
312
|
+
: `${baseUrl}/setup`;
|
|
274
313
|
if (!args.noBrowser) {
|
|
275
|
-
deps.openBrowser(
|
|
314
|
+
deps.openBrowser(signInUrl, logger);
|
|
315
|
+
} else if (loginToken) {
|
|
316
|
+
logger.log(`Sign in (link valid ~15 min, single use): ${signInUrl}`);
|
|
276
317
|
}
|
|
277
318
|
logger.log(`Done. First steps: ${baseUrl}/connect`);
|
|
278
319
|
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dashclaw/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DashClaw terminal client — approve agent actions and diagnose your instance",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"dashclaw",
|
|
8
|
-
"cli",
|
|
9
|
-
"claude-code",
|
|
10
|
-
"ai-governance",
|
|
11
|
-
"agent-governance"
|
|
12
|
-
],
|
|
13
|
-
"bin": {
|
|
14
|
-
"dashclaw": "./bin/dashclaw.js"
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"bin/",
|
|
18
|
-
"lib/",
|
|
19
|
-
"README.md"
|
|
20
|
-
],
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=18.0.0"
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"test": "node --test test/**/*.test.js"
|
|
26
|
-
},
|
|
27
|
-
"dependencies": {
|
|
28
|
-
"dashclaw": "^2.2.1",
|
|
29
|
-
"embedded-postgres": "18.4.0-beta.17",
|
|
30
|
-
"pg": "^8.22.0",
|
|
31
|
-
"tar": "^7.4.0"
|
|
32
|
-
},
|
|
33
|
-
"license": "MIT",
|
|
34
|
-
"publishConfig": {
|
|
35
|
-
"access": "public"
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@dashclaw/cli",
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "DashClaw terminal client — approve agent actions and diagnose your instance",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dashclaw",
|
|
8
|
+
"cli",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"ai-governance",
|
|
11
|
+
"agent-governance"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"dashclaw": "./bin/dashclaw.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"lib/",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test test/**/*.test.js"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"dashclaw": "^2.2.1",
|
|
29
|
+
"embedded-postgres": "18.4.0-beta.17",
|
|
30
|
+
"pg": "^8.22.0",
|
|
31
|
+
"tar": "^7.4.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/lib/code/apply.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
// `dashclaw code apply <manifestId>` — fetch a write plan from the server
|
|
2
|
-
// and apply it locally. The server emits the manifest (Phase 6) with a
|
|
3
|
-
// 24h TTL. This module is the only place the CLI writes to disk for the
|
|
4
|
-
// Optimal Files feature.
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { _ensureInsideProject, applyMerge } from './vendored.js';
|
|
9
|
-
|
|
10
|
-
const SECRET_PATTERNS = [
|
|
11
|
-
{ name: 'stripe_test', re: /sk_test_[A-Za-z0-9]{8,}/g },
|
|
12
|
-
{ name: 'stripe_live', re: /sk_live_[A-Za-z0-9]{8,}/g },
|
|
13
|
-
{ name: 'stripe_webhook', re: /whsec_[A-Za-z0-9]{8,}/g },
|
|
14
|
-
{ name: 'openai_key', re: /sk-[A-Za-z0-9]{20,}/g },
|
|
15
|
-
{ name: 'anthropic_key', re: /sk-ant-[A-Za-z0-9_\-]{20,}/g },
|
|
16
|
-
{ name: 'github_pat', re: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g },
|
|
17
|
-
{ name: 'aws_access', re: /AKIA[0-9A-Z]{16}/g },
|
|
18
|
-
{ name: 'jwt', re: /eyJ[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{20,}/g },
|
|
19
|
-
{ name: 'private_key', re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
20
|
-
{ name: 'env_assign', re: /\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|CRED|AUTH))\s*=\s*['"]?[^\s'"\n]+/g },
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
function scanForSecrets(content) {
|
|
24
|
-
let redactions = 0;
|
|
25
|
-
let out = String(content == null ? '' : content);
|
|
26
|
-
for (const p of SECRET_PATTERNS) {
|
|
27
|
-
out = out.replace(p.re, (_match, captured) => {
|
|
28
|
-
redactions++;
|
|
29
|
-
if (p.name === 'env_assign' && captured) return `${captured}=<REDACTED:${p.name}>`;
|
|
30
|
-
return `<REDACTED:${p.name}>`;
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
return { status: redactions === 0 ? 'passed' : 'redacted', redactions, redacted: out };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function fetchManifest(baseUrl, apiKey, manifestId, { fetchImpl = fetch }) {
|
|
37
|
-
const url = baseUrl.replace(/\/+$/, '') + '/api/code-sessions/manifests/' + encodeURIComponent(manifestId);
|
|
38
|
-
const res = await fetchImpl(url, {
|
|
39
|
-
headers: { 'x-api-key': apiKey },
|
|
40
|
-
});
|
|
41
|
-
let body = null;
|
|
42
|
-
try { body = await res.json(); } catch { /* null */ }
|
|
43
|
-
return { status: res.status, ok: res.ok, body };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function runApply({
|
|
47
|
-
baseUrl,
|
|
48
|
-
apiKey,
|
|
49
|
-
manifestId,
|
|
50
|
-
dest,
|
|
51
|
-
yes = false,
|
|
52
|
-
allowRedactions = false,
|
|
53
|
-
allowOverwriteSideBySide = false,
|
|
54
|
-
fetchImpl = fetch,
|
|
55
|
-
logger = console,
|
|
56
|
-
}) {
|
|
57
|
-
if (!manifestId) throw new Error('runApply: manifestId is required');
|
|
58
|
-
if (!dest) throw new Error('runApply: --dest=<dir> is required');
|
|
59
|
-
if (!baseUrl || !apiKey) throw new Error('runApply: baseUrl and apiKey are required');
|
|
60
|
-
|
|
61
|
-
const destAbs = path.resolve(dest);
|
|
62
|
-
try { fs.mkdirSync(destAbs, { recursive: true }); }
|
|
63
|
-
catch (err) { throw new Error('Could not create destination ' + destAbs + ': ' + err.message); }
|
|
64
|
-
|
|
65
|
-
const { status, ok, body } = await fetchManifest(baseUrl, apiKey, manifestId, { fetchImpl });
|
|
66
|
-
if (!ok) throw new Error('Failed to load manifest: HTTP ' + status + (body?.error ? ' — ' + body.error : ''));
|
|
67
|
-
const plan = Array.isArray(body?.plan) ? body.plan : Array.isArray(body?.plan?.results) ? body.plan.results : null;
|
|
68
|
-
if (!plan) throw new Error('Manifest payload missing plan');
|
|
69
|
-
|
|
70
|
-
const results = [];
|
|
71
|
-
for (const entry of plan) {
|
|
72
|
-
const target = entry.path;
|
|
73
|
-
if (!target) {
|
|
74
|
-
results.push({ path: '(unknown)', status: 'invalid_entry' });
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const abs = _ensureInsideProject(destAbs, target);
|
|
78
|
-
if (!abs) {
|
|
79
|
-
results.push({ path: target, status: 'unsafe_path' });
|
|
80
|
-
logger.warn(` ${target} -> unsafe_path (refused)`);
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const content = entry.content;
|
|
84
|
-
if (typeof content !== 'string') {
|
|
85
|
-
results.push({ path: target, status: 'no_content' });
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const scan = scanForSecrets(content);
|
|
89
|
-
if (scan.status === 'redacted' && !allowRedactions) {
|
|
90
|
-
results.push({ path: target, status: 'redacted', redactions: scan.redactions });
|
|
91
|
-
logger.warn(` ${target} -> ${scan.redactions} secret pattern(s) detected, refused (use --allow-redactions)`);
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const mode = entry.mode || 'create';
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
if (mode === 'merge') {
|
|
98
|
-
if (!fs.existsSync(abs)) {
|
|
99
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
100
|
-
fs.writeFileSync(abs, scan.redacted, 'utf8');
|
|
101
|
-
results.push({ path: target, status: 'created', bytes: Buffer.byteLength(scan.redacted, 'utf8') });
|
|
102
|
-
logger.info(` ${target} -> created`);
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
const existing = fs.readFileSync(abs, 'utf8');
|
|
106
|
-
const merged = applyMerge(existing, scan.redacted, {
|
|
107
|
-
acceptedHeadings: entry.acceptedHeadings || [],
|
|
108
|
-
acceptedBullets: entry.acceptedBullets || [],
|
|
109
|
-
});
|
|
110
|
-
fs.writeFileSync(abs, merged.merged, 'utf8');
|
|
111
|
-
results.push({ path: target, status: 'merged', additions: merged.additions });
|
|
112
|
-
logger.info(` ${target} -> merged`);
|
|
113
|
-
} else if (mode === 'side_by_side') {
|
|
114
|
-
const sideAbs = entry.absolutePath
|
|
115
|
-
? path.resolve(entry.absolutePath)
|
|
116
|
-
: sideBySidePath(abs);
|
|
117
|
-
const guardedSide = _ensureInsideProject(destAbs, path.relative(destAbs, sideAbs));
|
|
118
|
-
if (!guardedSide) {
|
|
119
|
-
results.push({ path: target, status: 'unsafe_path' });
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
fs.mkdirSync(path.dirname(guardedSide), { recursive: true });
|
|
123
|
-
if (fs.existsSync(guardedSide) && !allowOverwriteSideBySide) {
|
|
124
|
-
results.push({ path: target, status: 'side_by_side_conflict' });
|
|
125
|
-
logger.warn(` ${target} -> side_by_side_conflict (pass --overwrite to replace)`);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
fs.writeFileSync(guardedSide, scan.redacted, 'utf8');
|
|
129
|
-
results.push({ path: target, status: 'side_by_side', bytes: Buffer.byteLength(scan.redacted, 'utf8') });
|
|
130
|
-
logger.info(` ${target} -> side_by_side`);
|
|
131
|
-
} else if (mode === 'skip') {
|
|
132
|
-
results.push({ path: target, status: 'skipped' });
|
|
133
|
-
} else {
|
|
134
|
-
// 'create' or 'overwrite'
|
|
135
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
136
|
-
const existed = fs.existsSync(abs);
|
|
137
|
-
if (existed && mode !== 'overwrite' && !yes) {
|
|
138
|
-
results.push({ path: target, status: 'refused_overwrite_without_yes' });
|
|
139
|
-
logger.warn(` ${target} -> exists; pass --yes or rebuild manifest with mode='overwrite'`);
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
fs.writeFileSync(abs, scan.redacted, 'utf8');
|
|
143
|
-
results.push({ path: target, status: existed ? 'overwritten' : 'created' });
|
|
144
|
-
logger.info(` ${target} -> ${existed ? 'overwritten' : 'created'}`);
|
|
145
|
-
}
|
|
146
|
-
} catch (err) {
|
|
147
|
-
results.push({ path: target, status: 'error', error: err.message });
|
|
148
|
-
logger.warn(` ${target} -> error: ${err.message}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return results;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function sideBySidePath(abs) {
|
|
155
|
-
const dir = path.dirname(abs);
|
|
156
|
-
const base = path.basename(abs);
|
|
157
|
-
const ext = path.extname(base);
|
|
158
|
-
if (!ext) return path.join(dir, base + '.NEW');
|
|
159
|
-
return path.join(dir, base.slice(0, -ext.length) + '.NEW' + ext);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Exported for the sync script + tests that want to assert the scan layer
|
|
163
|
-
// stays in lock-step with app/lib/claude-code/optimal-files/secret-scan.js.
|
|
164
|
-
export { scanForSecrets };
|