clementine-agent 1.1.11 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent/assistant.js
CHANGED
|
@@ -1517,18 +1517,31 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1517
1517
|
const { detectFrustrationSignals, detectRepeatedTopics } = require('./insight-engine.js');
|
|
1518
1518
|
const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
1519
1519
|
const since7d = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
1520
|
-
|
|
1521
|
-
|
|
1520
|
+
let recent = this.getRecentActivity(since24h, 50);
|
|
1521
|
+
let week = this.getRecentActivity(since7d, 200);
|
|
1522
|
+
// Phase 10c: per-agent scope filter. Per-agent bot session keys
|
|
1523
|
+
// embed the agent slug (e.g. dm:ross-the-sdr:userId), so when this
|
|
1524
|
+
// prompt is for a specific agent profile we only consider sessions
|
|
1525
|
+
// that involved THAT agent. Without this filter, Nate's frustration
|
|
1526
|
+
// chatting with Sasha would leak into Ross's prompt — wrong signal.
|
|
1527
|
+
if (profile?.slug) {
|
|
1528
|
+
const slugMarker = `:${profile.slug}:`;
|
|
1529
|
+
recent = recent.filter(e => e.sessionKey.includes(slugMarker));
|
|
1530
|
+
week = week.filter(e => e.sessionKey.includes(slugMarker));
|
|
1531
|
+
}
|
|
1522
1532
|
const frustration = detectFrustrationSignals(recent);
|
|
1523
1533
|
const topics = detectRepeatedTopics(week);
|
|
1524
1534
|
const allSignals = [...frustration, ...topics];
|
|
1525
1535
|
if (allSignals.length > 0) {
|
|
1536
|
+
const scopeNote = profile?.slug
|
|
1537
|
+
? `\n\n*Scope: signals from sessions with you (${profile.slug}).*`
|
|
1538
|
+
: '';
|
|
1526
1539
|
const guidance = frustration.length > 0
|
|
1527
1540
|
? '\n\n**Adjust your approach:** When friction signals are present, lead with a clarifying question instead of assuming. Acknowledge the prior misunderstanding briefly without over-apologizing. Confirm understanding before acting.'
|
|
1528
1541
|
: '\n\n**Use this context naturally:** Recurring topics may indicate an unresolved thread — if relevant, offer to close the loop or summarize current state. Do not force callbacks if not directly applicable.';
|
|
1529
1542
|
volatileParts.push(`## Conversational Context\n\nSignals from recent sessions:\n` +
|
|
1530
1543
|
allSignals.map(s => `- ${s}`).join('\n') +
|
|
1531
|
-
guidance);
|
|
1544
|
+
guidance + scopeNote);
|
|
1532
1545
|
}
|
|
1533
1546
|
}
|
|
1534
1547
|
catch { /* non-fatal — insight-engine optional */ }
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -16981,6 +16981,51 @@ async function refreshToolUsagePanel() {
|
|
|
16981
16981
|
}
|
|
16982
16982
|
html += '</table>';
|
|
16983
16983
|
}
|
|
16984
|
+
|
|
16985
|
+
// Phase 11e: cost-by-source pivot — aggregates the bySource maps from
|
|
16986
|
+
// every family into a single "top jobs by spend" view. Uses only data
|
|
16987
|
+
// already in the response, no extra API call.
|
|
16988
|
+
const sourceTotals = {};
|
|
16989
|
+
for (const f of (data.families || [])) {
|
|
16990
|
+
const callsPerSource = {};
|
|
16991
|
+
for (const s of (f.bySource || [])) callsPerSource[s.source] = s.count;
|
|
16992
|
+
const familyTotalCalls = f.totalCalls || 0;
|
|
16993
|
+
for (const s of (f.bySource || [])) {
|
|
16994
|
+
const share = familyTotalCalls > 0 ? s.count / familyTotalCalls : 0;
|
|
16995
|
+
const cost = (f.estimatedCostUsd || 0) * share;
|
|
16996
|
+
if (!sourceTotals[s.source]) sourceTotals[s.source] = { source: s.source, cost: 0, calls: 0 };
|
|
16997
|
+
sourceTotals[s.source].cost += cost;
|
|
16998
|
+
sourceTotals[s.source].calls += s.count;
|
|
16999
|
+
}
|
|
17000
|
+
}
|
|
17001
|
+
const sourceList = Object.values(sourceTotals)
|
|
17002
|
+
.filter(s => s.source !== 'unknown' || sourceTotals.unknown.cost > 0)
|
|
17003
|
+
.sort((a, b) => b.cost - a.cost);
|
|
17004
|
+
|
|
17005
|
+
if (sourceList.length > 0) {
|
|
17006
|
+
const maxSrcCost = Math.max.apply(null, sourceList.map(s => s.cost).concat([0.0001]));
|
|
17007
|
+
html += '<div style="margin-top:18px"><div style="font-weight:600;font-size:13px;margin-bottom:8px;color:var(--text-secondary)">Top jobs by cost</div>';
|
|
17008
|
+
html += '<table style="width:100%;font-size:13px"><tr>'
|
|
17009
|
+
+ '<th>Job / source</th><th style="text-align:right">Cost</th><th style="text-align:right">Share</th><th style="text-align:right">Calls</th><th>Distribution</th></tr>';
|
|
17010
|
+
const totalSrcCost = sourceList.reduce((sum, s) => sum + s.cost, 0);
|
|
17011
|
+
for (const s of sourceList.slice(0, 10)) {
|
|
17012
|
+
const pct = totalSrcCost > 0 ? ((s.cost / totalSrcCost) * 100).toFixed(1) + '%' : '0.0%';
|
|
17013
|
+
const barW = Math.max(2, Math.round((s.cost / maxSrcCost) * 100));
|
|
17014
|
+
const sourceLabel = s.source === 'unknown'
|
|
17015
|
+
? '<em style="color:var(--text-muted)">unattributed</em>'
|
|
17016
|
+
: '<strong>' + esc(s.source) + '</strong>';
|
|
17017
|
+
html += '<tr>'
|
|
17018
|
+
+ '<td>' + sourceLabel + '</td>'
|
|
17019
|
+
+ '<td style="text-align:right;color:var(--green)">$' + s.cost.toFixed(2) + '</td>'
|
|
17020
|
+
+ '<td style="text-align:right;color:var(--text-muted)">' + pct + '</td>'
|
|
17021
|
+
+ '<td style="text-align:right">' + s.calls.toLocaleString() + '</td>'
|
|
17022
|
+
+ '<td><div style="background:var(--bg-elev);height:8px;border-radius:4px;overflow:hidden;width:100%;max-width:160px">'
|
|
17023
|
+
+ '<div style="background:var(--blue);height:100%;width:' + barW + '%"></div></div></td>'
|
|
17024
|
+
+ '</tr>';
|
|
17025
|
+
}
|
|
17026
|
+
html += '</table></div>';
|
|
17027
|
+
}
|
|
17028
|
+
|
|
16984
17029
|
html += '</div></div>';
|
|
16985
17030
|
host.innerHTML = html;
|
|
16986
17031
|
} catch(e) {
|
package/dist/cli/index.js
CHANGED
|
@@ -899,7 +899,10 @@ function cmdConfigSet(key, value) {
|
|
|
899
899
|
else {
|
|
900
900
|
content = content.trimEnd() + `\n${upperKey}=${value}\n`;
|
|
901
901
|
}
|
|
902
|
-
|
|
902
|
+
// Mode 0o600 — credentials live here; never world-readable. Phase 12
|
|
903
|
+
// hardening also fixes pre-existing .env files via `clementine config
|
|
904
|
+
// harden-permissions`.
|
|
905
|
+
writeFileSync(ENV_PATH, content, { mode: 0o600 });
|
|
903
906
|
console.log(` Set ${upperKey}=${value}`);
|
|
904
907
|
}
|
|
905
908
|
function cmdConfigGet(key) {
|
|
@@ -1217,6 +1220,65 @@ async function cmdConfigMigrateFromKeychain(opts) {
|
|
|
1217
1220
|
console.log(` ${GREEN}Migrated ${result.migrated.length} key${result.migrated.length === 1 ? '' : 's'} out of keychain.${RESET}`);
|
|
1218
1221
|
console.log();
|
|
1219
1222
|
}
|
|
1223
|
+
// ── Config harden-permissions ────────────────────────────────────────
|
|
1224
|
+
async function cmdConfigHardenPermissions(opts) {
|
|
1225
|
+
const { hardenPermissions } = await import('../config/harden-permissions.js');
|
|
1226
|
+
const report = hardenPermissions(BASE_DIR, { dryRun: opts.dryRun });
|
|
1227
|
+
if (opts.json) {
|
|
1228
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1229
|
+
process.exit(report.failed > 0 ? 1 : 0);
|
|
1230
|
+
}
|
|
1231
|
+
const DIM = '\x1b[0;90m';
|
|
1232
|
+
const BOLD = '\x1b[1m';
|
|
1233
|
+
const GREEN = '\x1b[0;32m';
|
|
1234
|
+
const YELLOW = '\x1b[0;33m';
|
|
1235
|
+
const RED = '\x1b[0;31m';
|
|
1236
|
+
const CYAN = '\x1b[0;36m';
|
|
1237
|
+
const RESET = '\x1b[0m';
|
|
1238
|
+
console.log();
|
|
1239
|
+
console.log(` ${BOLD}Data home:${RESET} ${report.baseDir}`);
|
|
1240
|
+
console.log(` ${BOLD}Mode:${RESET} ${opts.dryRun ? `${YELLOW}dry run${RESET}` : `${GREEN}apply${RESET}`}`);
|
|
1241
|
+
console.log();
|
|
1242
|
+
console.log(` ${BOLD}Scanned:${RESET} ${report.scanned}`);
|
|
1243
|
+
console.log(` ${GREEN}Tightened:${RESET} ${report.tightened}`);
|
|
1244
|
+
console.log(` ${DIM}Already correct:${RESET} ${report.alreadyCorrect}`);
|
|
1245
|
+
console.log(` ${DIM}Skipped:${RESET} ${report.skipped}`);
|
|
1246
|
+
if (report.failed > 0) {
|
|
1247
|
+
console.log(` ${RED}Failed:${RESET} ${report.failed}`);
|
|
1248
|
+
}
|
|
1249
|
+
console.log();
|
|
1250
|
+
// Show first ~20 tightened entries — enough to see the shape without flooding
|
|
1251
|
+
const tightened = report.entries.filter(e => e.status === 'tightened').slice(0, 20);
|
|
1252
|
+
if (tightened.length > 0) {
|
|
1253
|
+
console.log(` ${BOLD}${opts.dryRun ? 'Would tighten' : 'Tightened'} (showing first ${tightened.length}):${RESET}`);
|
|
1254
|
+
for (const e of tightened) {
|
|
1255
|
+
const rel = e.path.startsWith(report.baseDir + '/') ? e.path.slice(report.baseDir.length + 1) : e.path;
|
|
1256
|
+
const kindTag = e.kind === 'directory' ? `${CYAN}d${RESET}` : `${DIM}-${RESET}`;
|
|
1257
|
+
console.log(` ${kindTag} ${e.beforeMode} ${DIM}→${RESET} ${GREEN}${e.afterMode}${RESET} ${rel}`);
|
|
1258
|
+
}
|
|
1259
|
+
if (report.tightened > tightened.length) {
|
|
1260
|
+
console.log(` ${DIM}... ${report.tightened - tightened.length} more${RESET}`);
|
|
1261
|
+
}
|
|
1262
|
+
console.log();
|
|
1263
|
+
}
|
|
1264
|
+
const failed = report.entries.filter(e => e.status === 'failed').slice(0, 10);
|
|
1265
|
+
if (failed.length > 0) {
|
|
1266
|
+
console.log(` ${RED}${BOLD}Failed:${RESET}`);
|
|
1267
|
+
for (const e of failed) {
|
|
1268
|
+
console.log(` ${RED}✗${RESET} ${e.path} — ${e.error ?? 'unknown'}`);
|
|
1269
|
+
}
|
|
1270
|
+
console.log();
|
|
1271
|
+
}
|
|
1272
|
+
if (opts.dryRun) {
|
|
1273
|
+
console.log(` ${DIM}Re-run without --dry-run to apply.${RESET}`);
|
|
1274
|
+
console.log();
|
|
1275
|
+
}
|
|
1276
|
+
else if (report.tightened > 0) {
|
|
1277
|
+
console.log(` ${GREEN}Done.${RESET} ${DIM}Re-run \`clementine config doctor\` to confirm permissions are now correct.${RESET}`);
|
|
1278
|
+
console.log();
|
|
1279
|
+
}
|
|
1280
|
+
process.exit(report.failed > 0 ? 1 : 0);
|
|
1281
|
+
}
|
|
1220
1282
|
// ── Config keychain-fix-acl ─────────────────────────────────────────
|
|
1221
1283
|
async function cmdConfigKeychainFixAcl(opts) {
|
|
1222
1284
|
const { listClementineKeychainEntries, fixAllClementineEntries } = await import('../config/keychain-fix-acl.js');
|
|
@@ -2004,6 +2066,14 @@ configCmd
|
|
|
2004
2066
|
.action(async (opts) => {
|
|
2005
2067
|
await cmdConfigKeychainFixAcl(opts);
|
|
2006
2068
|
});
|
|
2069
|
+
configCmd
|
|
2070
|
+
.command('harden-permissions')
|
|
2071
|
+
.description('Tighten file modes in ~/.clementine/ — files to 0600, directories to 0700')
|
|
2072
|
+
.option('--dry-run', 'Preview without changing anything')
|
|
2073
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2074
|
+
.action(async (opts) => {
|
|
2075
|
+
await cmdConfigHardenPermissions(opts);
|
|
2076
|
+
});
|
|
2007
2077
|
configCmd
|
|
2008
2078
|
.command('edit')
|
|
2009
2079
|
.description('Open .env in your editor')
|
|
@@ -2229,10 +2299,10 @@ async function cmdUpdate(options) {
|
|
|
2229
2299
|
console.log(` ${S()} Backing up config...`);
|
|
2230
2300
|
if (!options.dryRun) {
|
|
2231
2301
|
mkdirSync(backupDir, { recursive: true });
|
|
2232
|
-
// .env
|
|
2302
|
+
// .env (mode 0o600 — backup carries credentials, same protection as live file)
|
|
2233
2303
|
if (existsSync(ENV_PATH)) {
|
|
2234
2304
|
const envContent = readFileSync(ENV_PATH, 'utf-8');
|
|
2235
|
-
writeFileSync(path.join(backupDir, '.env'), envContent);
|
|
2305
|
+
writeFileSync(path.join(backupDir, '.env'), envContent, { mode: 0o600 });
|
|
2236
2306
|
}
|
|
2237
2307
|
// Cron state
|
|
2238
2308
|
const cronStateFile = path.join(BASE_DIR, '.cron_last_run.json');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tighten file modes on the Clementine data home.
|
|
3
|
+
*
|
|
4
|
+
* Walks ~/.clementine/ (or any baseDir) and:
|
|
5
|
+
* - Sets every regular file to mode 0600 (owner read/write only)
|
|
6
|
+
* - Sets every directory to mode 0700 (owner traverse only)
|
|
7
|
+
*
|
|
8
|
+
* Why: state files live alongside `credentials.json` and `.env`. Even
|
|
9
|
+
* after Phase 9b's outbound redaction, contents on disk include
|
|
10
|
+
* conversation transcripts, advisor LLM outputs, cron run logs, full
|
|
11
|
+
* Discord/Slack message snippets, etc. Default umask 022 leaves all of
|
|
12
|
+
* those world-readable; on a single-user laptop the practical risk is
|
|
13
|
+
* Time Machine backup exposure and any process running as the user.
|
|
14
|
+
*
|
|
15
|
+
* Pure: returns a result describing what was tightened, what was already
|
|
16
|
+
* correct, and what failed (e.g. permission denied if user wasn't owner).
|
|
17
|
+
* Caller decides whether to print, JSON-stringify, or act on the result.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: re-running on an already-hardened tree is a no-op (every
|
|
20
|
+
* entry returns "already correct"). chmod is the cheapest syscall —
|
|
21
|
+
* even a 1000-file tree completes in milliseconds.
|
|
22
|
+
*
|
|
23
|
+
* Skip list: a few entries are intentionally NOT touched:
|
|
24
|
+
* - Symlinks (chmod follows symlinks; could escalate elsewhere). We
|
|
25
|
+
* check via lstat and skip non-files/non-dirs.
|
|
26
|
+
* - Files we don't own (chmod will fail; we capture the failure in
|
|
27
|
+
* the report rather than crashing).
|
|
28
|
+
*/
|
|
29
|
+
export type HardenStatus = 'tightened' | 'already-correct' | 'skipped' | 'failed';
|
|
30
|
+
export interface HardenEntry {
|
|
31
|
+
path: string;
|
|
32
|
+
kind: 'file' | 'directory' | 'other';
|
|
33
|
+
beforeMode: string;
|
|
34
|
+
afterMode: string;
|
|
35
|
+
status: HardenStatus;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface HardenReport {
|
|
39
|
+
baseDir: string;
|
|
40
|
+
scanned: number;
|
|
41
|
+
tightened: number;
|
|
42
|
+
alreadyCorrect: number;
|
|
43
|
+
skipped: number;
|
|
44
|
+
failed: number;
|
|
45
|
+
/** Per-entry detail. Cap at 50 in printable form to keep output sane;
|
|
46
|
+
* full list available in the JSON output. */
|
|
47
|
+
entries: HardenEntry[];
|
|
48
|
+
}
|
|
49
|
+
export declare function hardenPermissions(baseDir: string, opts?: {
|
|
50
|
+
dryRun?: boolean;
|
|
51
|
+
}): HardenReport;
|
|
52
|
+
//# sourceMappingURL=harden-permissions.d.ts.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tighten file modes on the Clementine data home.
|
|
3
|
+
*
|
|
4
|
+
* Walks ~/.clementine/ (or any baseDir) and:
|
|
5
|
+
* - Sets every regular file to mode 0600 (owner read/write only)
|
|
6
|
+
* - Sets every directory to mode 0700 (owner traverse only)
|
|
7
|
+
*
|
|
8
|
+
* Why: state files live alongside `credentials.json` and `.env`. Even
|
|
9
|
+
* after Phase 9b's outbound redaction, contents on disk include
|
|
10
|
+
* conversation transcripts, advisor LLM outputs, cron run logs, full
|
|
11
|
+
* Discord/Slack message snippets, etc. Default umask 022 leaves all of
|
|
12
|
+
* those world-readable; on a single-user laptop the practical risk is
|
|
13
|
+
* Time Machine backup exposure and any process running as the user.
|
|
14
|
+
*
|
|
15
|
+
* Pure: returns a result describing what was tightened, what was already
|
|
16
|
+
* correct, and what failed (e.g. permission denied if user wasn't owner).
|
|
17
|
+
* Caller decides whether to print, JSON-stringify, or act on the result.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: re-running on an already-hardened tree is a no-op (every
|
|
20
|
+
* entry returns "already correct"). chmod is the cheapest syscall —
|
|
21
|
+
* even a 1000-file tree completes in milliseconds.
|
|
22
|
+
*
|
|
23
|
+
* Skip list: a few entries are intentionally NOT touched:
|
|
24
|
+
* - Symlinks (chmod follows symlinks; could escalate elsewhere). We
|
|
25
|
+
* check via lstat and skip non-files/non-dirs.
|
|
26
|
+
* - Files we don't own (chmod will fail; we capture the failure in
|
|
27
|
+
* the report rather than crashing).
|
|
28
|
+
*/
|
|
29
|
+
import { readdirSync, lstatSync, chmodSync } from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
const FILE_TARGET = 0o600;
|
|
32
|
+
const DIR_TARGET = 0o700;
|
|
33
|
+
function octal(mode) {
|
|
34
|
+
return (mode & 0o777).toString(8).padStart(3, '0');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Walk a directory tree iteratively (not recursively — no stack-blow risk
|
|
38
|
+
* on deep vault trees). Returns absolute paths.
|
|
39
|
+
*/
|
|
40
|
+
function walk(root) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const stack = [root];
|
|
43
|
+
while (stack.length > 0) {
|
|
44
|
+
const dir = stack.pop();
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(dir);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue; // unreadable dir — caller will see it as skipped
|
|
51
|
+
}
|
|
52
|
+
for (const name of entries) {
|
|
53
|
+
const full = path.join(dir, name);
|
|
54
|
+
try {
|
|
55
|
+
const st = lstatSync(full);
|
|
56
|
+
if (st.isSymbolicLink())
|
|
57
|
+
continue; // never follow
|
|
58
|
+
if (st.isDirectory()) {
|
|
59
|
+
out.push(full);
|
|
60
|
+
stack.push(full);
|
|
61
|
+
}
|
|
62
|
+
else if (st.isFile()) {
|
|
63
|
+
out.push(full);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// stat failed — skip
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
export function hardenPermissions(baseDir, opts = {}) {
|
|
74
|
+
const report = {
|
|
75
|
+
baseDir,
|
|
76
|
+
scanned: 0,
|
|
77
|
+
tightened: 0,
|
|
78
|
+
alreadyCorrect: 0,
|
|
79
|
+
skipped: 0,
|
|
80
|
+
failed: 0,
|
|
81
|
+
entries: [],
|
|
82
|
+
};
|
|
83
|
+
// Always include baseDir itself in the walk
|
|
84
|
+
let baseSt;
|
|
85
|
+
try {
|
|
86
|
+
baseSt = lstatSync(baseDir);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
report.entries.push({
|
|
90
|
+
path: baseDir,
|
|
91
|
+
kind: 'other',
|
|
92
|
+
beforeMode: '???',
|
|
93
|
+
afterMode: '???',
|
|
94
|
+
status: 'failed',
|
|
95
|
+
error: `baseDir not accessible: ${String(err).slice(0, 100)}`,
|
|
96
|
+
});
|
|
97
|
+
report.failed++;
|
|
98
|
+
return report;
|
|
99
|
+
}
|
|
100
|
+
if (!baseSt.isDirectory()) {
|
|
101
|
+
report.entries.push({
|
|
102
|
+
path: baseDir,
|
|
103
|
+
kind: 'other',
|
|
104
|
+
beforeMode: octal(baseSt.mode),
|
|
105
|
+
afterMode: octal(baseSt.mode),
|
|
106
|
+
status: 'skipped',
|
|
107
|
+
error: 'baseDir is not a directory',
|
|
108
|
+
});
|
|
109
|
+
report.skipped++;
|
|
110
|
+
return report;
|
|
111
|
+
}
|
|
112
|
+
const all = [baseDir, ...walk(baseDir)];
|
|
113
|
+
for (const p of all) {
|
|
114
|
+
let st;
|
|
115
|
+
try {
|
|
116
|
+
st = lstatSync(p);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
report.entries.push({
|
|
120
|
+
path: p,
|
|
121
|
+
kind: 'other',
|
|
122
|
+
beforeMode: '???',
|
|
123
|
+
afterMode: '???',
|
|
124
|
+
status: 'failed',
|
|
125
|
+
error: String(err).slice(0, 100),
|
|
126
|
+
});
|
|
127
|
+
report.failed++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
report.scanned++;
|
|
131
|
+
const beforeMode = octal(st.mode);
|
|
132
|
+
let kind = 'other';
|
|
133
|
+
let target = null;
|
|
134
|
+
if (st.isDirectory()) {
|
|
135
|
+
kind = 'directory';
|
|
136
|
+
target = DIR_TARGET;
|
|
137
|
+
}
|
|
138
|
+
else if (st.isFile()) {
|
|
139
|
+
kind = 'file';
|
|
140
|
+
target = FILE_TARGET;
|
|
141
|
+
}
|
|
142
|
+
if (target === null) {
|
|
143
|
+
// Sockets, FIFOs, devices, etc. — leave alone.
|
|
144
|
+
report.entries.push({
|
|
145
|
+
path: p, kind, beforeMode, afterMode: beforeMode, status: 'skipped',
|
|
146
|
+
});
|
|
147
|
+
report.skipped++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if ((st.mode & 0o777) === target) {
|
|
151
|
+
report.entries.push({
|
|
152
|
+
path: p, kind, beforeMode, afterMode: beforeMode, status: 'already-correct',
|
|
153
|
+
});
|
|
154
|
+
report.alreadyCorrect++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (opts.dryRun) {
|
|
158
|
+
report.entries.push({
|
|
159
|
+
path: p, kind, beforeMode, afterMode: octal(target), status: 'tightened',
|
|
160
|
+
});
|
|
161
|
+
report.tightened++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
chmodSync(p, target);
|
|
166
|
+
report.entries.push({
|
|
167
|
+
path: p, kind, beforeMode, afterMode: octal(target), status: 'tightened',
|
|
168
|
+
});
|
|
169
|
+
report.tightened++;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
report.entries.push({
|
|
173
|
+
path: p, kind, beforeMode, afterMode: beforeMode, status: 'failed',
|
|
174
|
+
error: String(err).slice(0, 100),
|
|
175
|
+
});
|
|
176
|
+
report.failed++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return report;
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=harden-permissions.js.map
|