@theglitchking/semantic-memory 1.5.1 → 1.5.3
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +43 -0
- package/README.md +4 -0
- package/dist/{chunk-25EIQWXL.js → chunk-44BQDRXD.js} +5 -5
- package/dist/chunk-44BQDRXD.js.map +1 -0
- package/dist/{chunk-2EELGTLX.js → chunk-D5QLNNA4.js} +2 -2
- package/dist/{chunk-A3KYHKB5.js → chunk-FTRXH2WC.js} +8 -4
- package/dist/chunk-FTRXH2WC.js.map +1 -0
- package/dist/{chunk-OWZWMB4P.js → chunk-FURCIJBO.js} +1 -1
- package/dist/chunk-FURCIJBO.js.map +1 -0
- package/dist/{chunk-V45NJIHI.js → chunk-IGHCVAAL.js} +19 -4
- package/dist/chunk-IGHCVAAL.js.map +1 -0
- package/dist/{chunk-S4KW7VH4.js → chunk-LGQPP5FQ.js} +8 -3
- package/dist/chunk-LGQPP5FQ.js.map +1 -0
- package/dist/{chunk-NTYYS53O.js → chunk-NBY4TRP7.js} +7 -3
- package/dist/chunk-NBY4TRP7.js.map +1 -0
- package/dist/chunk-OEF5U5GV.js +175 -0
- package/dist/chunk-OEF5U5GV.js.map +1 -0
- package/dist/{chunk-FM26NFTR.js → chunk-OFXQXVG5.js} +26 -12
- package/dist/chunk-OFXQXVG5.js.map +1 -0
- package/dist/{chunk-CSXZ7QUU.js → chunk-ROKSUJFM.js} +27 -17
- package/dist/chunk-ROKSUJFM.js.map +1 -0
- package/dist/{chunk-JPQHNG3R.js → chunk-SX427M2R.js} +10 -4
- package/dist/chunk-SX427M2R.js.map +1 -0
- package/dist/{chunk-7M3JZOYH.js → chunk-WZEKIUYG.js} +8 -4
- package/dist/chunk-WZEKIUYG.js.map +1 -0
- package/dist/{chunk-WSU4QIPB.js → chunk-XAXK7NM4.js} +14 -8
- package/dist/chunk-XAXK7NM4.js.map +1 -0
- package/dist/cli/index.js +15 -15
- package/dist/core/index.d.ts +24 -4
- package/dist/core/index.js +20 -12
- package/dist/core/redact.d.ts +36 -0
- package/dist/core/redact.js +18 -0
- package/dist/{dossier-2UG724LB.js → dossier-RWTIMGZN.js} +4 -3
- package/dist/healthcheck-A32IWPNV.js +20 -0
- package/dist/indexer-RAKJ7JR3.js +8 -0
- package/dist/{lexicon-5PXQEMUF.js → lexicon-IVNJTMWE.js} +5 -4
- package/dist/lint-HURYGO3V.js +17 -0
- package/dist/log-YIXF6CYM.js +11 -0
- package/dist/mcp/server.js +32 -27
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-LXUEF7TX.js → profile-UE4YVTDC.js} +5 -4
- package/dist/{schema-ATFCLC6Y.js → schema-LY6533NJ.js} +2 -2
- package/dist/{search-AHKEFI2B.js → search-ZS6OW74K.js} +3 -3
- package/dist/{telemetry-GB2TEHVE.js → telemetry-B4KP3UVE.js} +4 -3
- package/dist/telemetry-B4KP3UVE.js.map +1 -0
- package/hooks/vault-context.js +112 -58
- package/package.json +10 -10
- package/dist/chunk-25EIQWXL.js.map +0 -1
- package/dist/chunk-7M3JZOYH.js.map +0 -1
- package/dist/chunk-A3KYHKB5.js.map +0 -1
- package/dist/chunk-CSXZ7QUU.js.map +0 -1
- package/dist/chunk-FM26NFTR.js.map +0 -1
- package/dist/chunk-JPQHNG3R.js.map +0 -1
- package/dist/chunk-NTYYS53O.js.map +0 -1
- package/dist/chunk-OWZWMB4P.js.map +0 -1
- package/dist/chunk-S4KW7VH4.js.map +0 -1
- package/dist/chunk-V45NJIHI.js.map +0 -1
- package/dist/chunk-WSU4QIPB.js.map +0 -1
- package/dist/healthcheck-IMB47LZE.js +0 -19
- package/dist/indexer-YW4O3VVM.js +0 -8
- package/dist/lint-XUQ7JDYX.js +0 -16
- package/dist/log-LQXIJXIA.js +0 -10
- /package/dist/{chunk-2EELGTLX.js.map → chunk-D5QLNNA4.js.map} +0 -0
- /package/dist/{dossier-2UG724LB.js.map → core/redact.js.map} +0 -0
- /package/dist/{healthcheck-IMB47LZE.js.map → dossier-RWTIMGZN.js.map} +0 -0
- /package/dist/{indexer-YW4O3VVM.js.map → healthcheck-A32IWPNV.js.map} +0 -0
- /package/dist/{lexicon-5PXQEMUF.js.map → indexer-RAKJ7JR3.js.map} +0 -0
- /package/dist/{lint-XUQ7JDYX.js.map → lexicon-IVNJTMWE.js.map} +0 -0
- /package/dist/{log-LQXIJXIA.js.map → lint-HURYGO3V.js.map} +0 -0
- /package/dist/{profile-LXUEF7TX.js.map → log-YIXF6CYM.js.map} +0 -0
- /package/dist/{schema-ATFCLC6Y.js.map → profile-UE4YVTDC.js.map} +0 -0
- /package/dist/{telemetry-GB2TEHVE.js.map → schema-LY6533NJ.js.map} +0 -0
- /package/dist/{search-AHKEFI2B.js.map → search-ZS6OW74K.js.map} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// src/core/redact.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var REDACTED = "[REDACTED]";
|
|
6
|
+
var SECRET_ENV_NAME = /key|token|secret|passw|pwd|credential/i;
|
|
7
|
+
var SECRET_WORD = String.raw`(?:api[_-]?)?(?:key|token|secret|password|passwd|pwd|credentials?)`;
|
|
8
|
+
var SECRET_NAME = String.raw`\b(?:[\w.]{0,64}[_.-])?${SECRET_WORD}`;
|
|
9
|
+
var VALUE = String.raw`[^\s"'\`,;&|()<>\[\]{}]`;
|
|
10
|
+
var PLACEHOLDER = /^(?:<[^>]*>|\$\{?\w+\}?|(?:process\.env|os\.environ|env)\b.*|\[REDACTED\]|(.)\1{3,}|\*+|x{3,}.*|password|passwd|pass|secret|token|pwd)$|example|changeme|placeholder|your[-_]|dummy|redacted/i;
|
|
11
|
+
var isPlaceholder = (value) => PLACEHOLDER.test(value) || /\.\.\.|…|x{4}/i.test(value);
|
|
12
|
+
var looksRandom = (value) => /\d/.test(value) || /^[A-Za-z]{16,}$/.test(value);
|
|
13
|
+
var withDigit = (prefix, body, min) => String.raw`${prefix}(?=${body}*\d)${body}{${min},}`;
|
|
14
|
+
var VENDOR = new RegExp(
|
|
15
|
+
String.raw`\b(?:` + [
|
|
16
|
+
withDigit(String.raw`(?:sk|rk|pk)[-_]`, String.raw`[\w-]`, 8),
|
|
17
|
+
withDigit("whsec_", String.raw`\w`, 8),
|
|
18
|
+
withDigit("gh[pousr]_", String.raw`\w`, 20),
|
|
19
|
+
withDigit("github_pat_", String.raw`\w`, 20),
|
|
20
|
+
withDigit("glpat-", String.raw`[\w-]`, 16),
|
|
21
|
+
withDigit("xox[abprs]-", String.raw`[\w-]`, 10),
|
|
22
|
+
withDigit("re_", String.raw`\w`, 16),
|
|
23
|
+
withDigit("hf_", String.raw`\w`, 20),
|
|
24
|
+
withDigit("npm_", String.raw`\w`, 20),
|
|
25
|
+
withDigit("GOCSPX-", String.raw`[\w-]`, 20),
|
|
26
|
+
// Google OAuth client secret
|
|
27
|
+
withDigit(String.raw`hvs\.`, String.raw`[\w-]`, 20),
|
|
28
|
+
// HashiCorp Vault service token
|
|
29
|
+
String.raw`(?:AKIA|ASIA)[0-9A-Z]{16}\b`,
|
|
30
|
+
String.raw`AIza[\w-]{30,}`
|
|
31
|
+
].join("|") + ")",
|
|
32
|
+
"g"
|
|
33
|
+
);
|
|
34
|
+
function rules(mode) {
|
|
35
|
+
const minValue = mode === "precise" ? 8 : 4;
|
|
36
|
+
const isSecretValue = (value) => !isPlaceholder(value) && (mode === "broad" || looksRandom(value));
|
|
37
|
+
const precise = [
|
|
38
|
+
// PEM private keys, even when the END line was cut off
|
|
39
|
+
{
|
|
40
|
+
name: "private key",
|
|
41
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,
|
|
42
|
+
replace: () => REDACTED
|
|
43
|
+
},
|
|
44
|
+
// credentials inside URLs: scheme://user:pass@host
|
|
45
|
+
{
|
|
46
|
+
name: "URL credentials",
|
|
47
|
+
re: /(:\/\/)([^/\s:@]+):([^/\s@]+)@/g,
|
|
48
|
+
replace: (_m, scheme, user, pass) => !isSecretValue(pass) || pass === user ? null : `${scheme}${REDACTED}@`
|
|
49
|
+
},
|
|
50
|
+
// Authorization headers
|
|
51
|
+
{
|
|
52
|
+
name: "authorization header",
|
|
53
|
+
re: /\b(bearer|basic)(\s+)(?=[A-Za-z0-9._~+/=-]*\d)([A-Za-z0-9._~+/=-]{16,})/gi,
|
|
54
|
+
replace: (_m, scheme, space) => `${scheme}${space}${REDACTED}`
|
|
55
|
+
},
|
|
56
|
+
// name=value, name: value, "name": "value" where the name is a secret word
|
|
57
|
+
{
|
|
58
|
+
name: "secret-named value",
|
|
59
|
+
re: new RegExp(String.raw`(${SECRET_NAME}["']?\s*[:=]\s*["']?)(${VALUE}{${minValue},})`, "gi"),
|
|
60
|
+
replace: (_m, prefix, value) => isSecretValue(value) ? `${prefix}${REDACTED}` : null
|
|
61
|
+
},
|
|
62
|
+
// command-line flags: --token VALUE, --api-key=VALUE
|
|
63
|
+
{
|
|
64
|
+
name: "secret flag",
|
|
65
|
+
// a flag starts after whitespace/start, and its name is bounded: keeps long dash/hyphen runs linear
|
|
66
|
+
re: new RegExp(String.raw`((?<![\w-])--?[\w-]{0,64}?${SECRET_WORD}[ =])(${VALUE}{${minValue},})`, "gi"),
|
|
67
|
+
replace: (_m, flag, value) => isSecretValue(value) ? `${flag}${REDACTED}` : null
|
|
68
|
+
},
|
|
69
|
+
{ name: "vendor API key", re: VENDOR, replace: (m) => isPlaceholder(m) ? null : REDACTED },
|
|
70
|
+
// JWTs
|
|
71
|
+
{ name: "JWT", re: /\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}/g, replace: () => REDACTED }
|
|
72
|
+
];
|
|
73
|
+
if (mode === "precise") return precise;
|
|
74
|
+
return [
|
|
75
|
+
...precise,
|
|
76
|
+
// base64 blobs with mixed case and digits (the lookbehind starts a try only at a run's start: linear on huge blobs)
|
|
77
|
+
{
|
|
78
|
+
name: "base64 blob",
|
|
79
|
+
re: /(?<![A-Za-z0-9+/])(?=[A-Za-z0-9+/]*\d)(?=[A-Za-z0-9+/]*[A-Z])(?=[A-Za-z0-9+/]*[a-z])[A-Za-z0-9+/]{40,}={0,2}/g,
|
|
80
|
+
replace: () => REDACTED
|
|
81
|
+
},
|
|
82
|
+
// any other long token mixing letters and digits (hashes, ids: over-redaction is fine in logs)
|
|
83
|
+
{ name: "long token", re: /(?<![\w-])(?=[\w-]*\d)(?=[\w-]*[A-Za-z])[\w-]{32,}(?![\w-])/g, replace: () => REDACTED }
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
var knownSecrets = null;
|
|
87
|
+
function loadKnownSecrets() {
|
|
88
|
+
const values = /* @__PURE__ */ new Set();
|
|
89
|
+
for (const [k, v] of Object.entries(process.env)) if (v && SECRET_ENV_NAME.test(k)) values.add(v);
|
|
90
|
+
try {
|
|
91
|
+
const env = JSON.parse(readFileSync(join(homedir(), ".claude", "settings.json"), "utf8"))?.env ?? {};
|
|
92
|
+
for (const [k, v] of Object.entries(env)) if (SECRET_ENV_NAME.test(k)) values.add(String(v));
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
return [...values].filter((v) => v.length >= 12 && !/\s/.test(v) && !v.startsWith("/") && !v.includes("://") && !isPlaceholder(v)).sort((a, b) => b.length - a.length);
|
|
96
|
+
}
|
|
97
|
+
function resetRedactCache() {
|
|
98
|
+
knownSecrets = null;
|
|
99
|
+
}
|
|
100
|
+
function redact(text, mode = "precise") {
|
|
101
|
+
if (!text) return { text, count: 0 };
|
|
102
|
+
knownSecrets ??= loadKnownSecrets();
|
|
103
|
+
let count = 0;
|
|
104
|
+
let out = text;
|
|
105
|
+
for (const value of knownSecrets) {
|
|
106
|
+
const parts = out.split(value);
|
|
107
|
+
if (parts.length > 1) {
|
|
108
|
+
count += parts.length - 1;
|
|
109
|
+
out = parts.join(REDACTED);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const rule of rules(mode)) {
|
|
113
|
+
out = out.replace(rule.re, (match, ...args) => {
|
|
114
|
+
const groups = args.slice(0, -2).map((g) => typeof g === "string" ? g : "");
|
|
115
|
+
const replaced = rule.replace(match, ...groups);
|
|
116
|
+
if (replaced === null || replaced === match) return match;
|
|
117
|
+
count++;
|
|
118
|
+
return replaced;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return { text: out, count };
|
|
122
|
+
}
|
|
123
|
+
function findSecrets(text) {
|
|
124
|
+
knownSecrets ??= loadKnownSecrets();
|
|
125
|
+
const hits = [];
|
|
126
|
+
for (const value of knownSecrets) {
|
|
127
|
+
for (let i = text.indexOf(value); i !== -1; i = text.indexOf(value, i + value.length)) {
|
|
128
|
+
hits.push({ index: i, kind: "secret env value" });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const rule of rules("precise")) {
|
|
132
|
+
for (const m of text.matchAll(rule.re)) {
|
|
133
|
+
const replaced = rule.replace(m[0], ...m.slice(1).map((g) => g ?? ""));
|
|
134
|
+
if (replaced !== null && replaced !== m[0]) hits.push({ index: m.index ?? 0, kind: rule.name });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const h of hits.sort((a, b) => a.index - b.index)) {
|
|
140
|
+
const line = text.slice(0, h.index).split("\n").length;
|
|
141
|
+
const key = `${line}:${h.kind}`;
|
|
142
|
+
if (!seen.has(key)) {
|
|
143
|
+
seen.add(key);
|
|
144
|
+
out.push({ line, kind: h.kind });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
function redactDeep(value, mode = "broad") {
|
|
150
|
+
let count = 0;
|
|
151
|
+
const walk = (v) => {
|
|
152
|
+
if (typeof v === "string") {
|
|
153
|
+
const r = redact(v, mode);
|
|
154
|
+
count += r.count;
|
|
155
|
+
return r.text;
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
158
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
|
|
159
|
+
return v;
|
|
160
|
+
};
|
|
161
|
+
return { value: walk(value), count };
|
|
162
|
+
}
|
|
163
|
+
function redactedNote(count) {
|
|
164
|
+
return count > 0 ? ` (${count} secret${count === 1 ? "" : "s"} redacted)` : "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export {
|
|
168
|
+
REDACTED,
|
|
169
|
+
resetRedactCache,
|
|
170
|
+
redact,
|
|
171
|
+
findSecrets,
|
|
172
|
+
redactDeep,
|
|
173
|
+
redactedNote
|
|
174
|
+
};
|
|
175
|
+
//# sourceMappingURL=chunk-OEF5U5GV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/redact.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Secret redaction for everything semantic-memory writes to disk. Ported from jev-loop's\n * redact.py: (1) exact values of secret-named env vars on this machine, (2) patterns.\n *\n * Two pattern sets:\n * - \"precise\" (notes, frontmatter, dossiers, profile, lexicon): only shapes that are almost\n * never ordinary content, because a false positive in a note is data loss.\n * - \"broad\" (search log, log.md events, session state, capture-pending): precise plus long\n * base64 / letter-and-digit tokens; over-redaction is fine in logs.\n *\n * Cannot catch a secret written in plain prose (\"the password is hunter2\"). Never logs values.\n */\n\nexport const REDACTED = \"[REDACTED]\";\nexport type RedactMode = \"precise\" | \"broad\";\nexport interface RedactResult {\n text: string;\n count: number;\n}\n\nconst SECRET_ENV_NAME = /key|token|secret|passw|pwd|credential/i;\n// The secret word must start a name segment: api_key, DB_PASSWORD, client-secret, apikey; not monkey, tokens, tokenizer\nconst SECRET_WORD = String.raw`(?:api[_-]?)?(?:key|token|secret|password|passwd|pwd|credentials?)`;\n// prefix bounded so a long dotted run (a.b.c…) stays linear\nconst SECRET_NAME = String.raw`\\b(?:[\\w.]{0,64}[_.-])?${SECRET_WORD}`;\n// Value characters: stops at whitespace, quotes, separators and markdown punctuation\nconst VALUE = String.raw`[^\\s\"'\\`,;&|()<>\\[\\]{}]`;\n\n// Obvious placeholders in how-to notes are left alone: <your-key>, ${API_KEY}, process.env.X, changeme, xxxx, ****\nconst PLACEHOLDER =\n /^(?:<[^>]*>|\\$\\{?\\w+\\}?|(?:process\\.env|os\\.environ|env)\\b.*|\\[REDACTED\\]|(.)\\1{3,}|\\*+|x{3,}.*|password|passwd|pass|secret|token|pwd)$|example|changeme|placeholder|your[-_]|dummy|redacted/i;\nconst isPlaceholder = (value: string) => PLACEHOLDER.test(value) || /\\.\\.\\.|…|x{4}/i.test(value);\n// Precise mode: a value counts only if it looks random — contains a digit, or is a long run of\n// letters (a passphrase). Code in notes (`api_key = os.getenv`, `token: settings.get_decrypted_key`,\n// `password=self._key`) has dots/underscores and no digits, and must survive.\nconst looksRandom = (value: string) => /\\d/.test(value) || /^[A-Za-z]{16,}$/.test(value);\n\n// Vendor key prefixes. Word-shaped bodies must contain a digit, so identifiers like\n// pk_customer_orders or npm_package_version stay; real keys are random and always have digits.\nconst withDigit = (prefix: string, body: string, min: number) => String.raw`${prefix}(?=${body}*\\d)${body}{${min},}`;\nconst VENDOR = new RegExp(\n String.raw`\\b(?:` +\n [\n withDigit(String.raw`(?:sk|rk|pk)[-_]`, String.raw`[\\w-]`, 8),\n withDigit(\"whsec_\", String.raw`\\w`, 8),\n withDigit(\"gh[pousr]_\", String.raw`\\w`, 20),\n withDigit(\"github_pat_\", String.raw`\\w`, 20),\n withDigit(\"glpat-\", String.raw`[\\w-]`, 16),\n withDigit(\"xox[abprs]-\", String.raw`[\\w-]`, 10),\n withDigit(\"re_\", String.raw`\\w`, 16),\n withDigit(\"hf_\", String.raw`\\w`, 20),\n withDigit(\"npm_\", String.raw`\\w`, 20),\n withDigit(\"GOCSPX-\", String.raw`[\\w-]`, 20), // Google OAuth client secret\n withDigit(String.raw`hvs\\.`, String.raw`[\\w-]`, 20), // HashiCorp Vault service token\n String.raw`(?:AKIA|ASIA)[0-9A-Z]{16}\\b`,\n String.raw`AIza[\\w-]{30,}`,\n ].join(\"|\") +\n \")\",\n \"g\"\n);\n\ninterface Rule {\n name: string; // reported by lint_vault's secrets check\n re: RegExp;\n replace: (match: string, ...groups: string[]) => string | null; // null = leave as is\n}\n\nfunction rules(mode: RedactMode): Rule[] {\n const minValue = mode === \"precise\" ? 8 : 4;\n const isSecretValue = (value: string) => !isPlaceholder(value) && (mode === \"broad\" || looksRandom(value));\n const precise: Rule[] = [\n // PEM private keys, even when the END line was cut off\n {\n name: \"private key\",\n re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,\n replace: () => REDACTED,\n },\n // credentials inside URLs: scheme://user:pass@host\n {\n name: \"URL credentials\",\n re: /(:\\/\\/)([^/\\s:@]+):([^/\\s@]+)@/g,\n replace: (_m, scheme, user, pass) => (!isSecretValue(pass) || pass === user ? null : `${scheme}${REDACTED}@`),\n },\n // Authorization headers\n {\n name: \"authorization header\",\n re: /\\b(bearer|basic)(\\s+)(?=[A-Za-z0-9._~+/=-]*\\d)([A-Za-z0-9._~+/=-]{16,})/gi,\n replace: (_m, scheme, space) => `${scheme}${space}${REDACTED}`,\n },\n // name=value, name: value, \"name\": \"value\" where the name is a secret word\n {\n name: \"secret-named value\",\n re: new RegExp(String.raw`(${SECRET_NAME}[\"']?\\s*[:=]\\s*[\"']?)(${VALUE}{${minValue},})`, \"gi\"),\n replace: (_m, prefix, value) => (isSecretValue(value) ? `${prefix}${REDACTED}` : null),\n },\n // command-line flags: --token VALUE, --api-key=VALUE\n {\n name: \"secret flag\",\n // a flag starts after whitespace/start, and its name is bounded: keeps long dash/hyphen runs linear\n re: new RegExp(String.raw`((?<![\\w-])--?[\\w-]{0,64}?${SECRET_WORD}[ =])(${VALUE}{${minValue},})`, \"gi\"),\n replace: (_m, flag, value) => (isSecretValue(value) ? `${flag}${REDACTED}` : null),\n },\n { name: \"vendor API key\", re: VENDOR, replace: (m) => (isPlaceholder(m) ? null : REDACTED) },\n // JWTs\n { name: \"JWT\", re: /\\beyJ[\\w-]{8,}\\.[\\w-]{8,}\\.[\\w-]{8,}/g, replace: () => REDACTED },\n ];\n if (mode === \"precise\") return precise;\n return [\n ...precise,\n // base64 blobs with mixed case and digits (the lookbehind starts a try only at a run's start: linear on huge blobs)\n {\n name: \"base64 blob\",\n re: /(?<![A-Za-z0-9+/])(?=[A-Za-z0-9+/]*\\d)(?=[A-Za-z0-9+/]*[A-Z])(?=[A-Za-z0-9+/]*[a-z])[A-Za-z0-9+/]{40,}={0,2}/g,\n replace: () => REDACTED,\n },\n // any other long token mixing letters and digits (hashes, ids: over-redaction is fine in logs)\n { name: \"long token\", re: /(?<![\\w-])(?=[\\w-]*\\d)(?=[\\w-]*[A-Za-z])[\\w-]{32,}(?![\\w-])/g, replace: () => REDACTED },\n ];\n}\n\nlet knownSecrets: string[] | null = null;\n\n/** Exact values of secret-named variables on this machine (env + ~/.claude/settings.json env), longest first. */\nfunction loadKnownSecrets(): string[] {\n const values = new Set<string>();\n for (const [k, v] of Object.entries(process.env)) if (v && SECRET_ENV_NAME.test(k)) values.add(v);\n try {\n const env = JSON.parse(readFileSync(join(homedir(), \".claude\", \"settings.json\"), \"utf8\"))?.env ?? {};\n for (const [k, v] of Object.entries(env)) if (SECRET_ENV_NAME.test(k)) values.add(String(v));\n } catch {\n // no settings file: env vars only\n }\n // Values written into notes must look like secrets: long, no whitespace, not a path or URL\n return [...values]\n .filter((v) => v.length >= 12 && !/\\s/.test(v) && !v.startsWith(\"/\") && !v.includes(\"://\") && !isPlaceholder(v))\n .sort((a, b) => b.length - a.length);\n}\n\n/** Test seam: re-read env and settings on the next call. */\nexport function resetRedactCache(): void {\n knownSecrets = null;\n}\n\nexport function redact(text: string, mode: RedactMode = \"precise\"): RedactResult {\n if (!text) return { text, count: 0 };\n knownSecrets ??= loadKnownSecrets();\n let count = 0;\n let out = text;\n for (const value of knownSecrets) {\n const parts = out.split(value);\n if (parts.length > 1) {\n count += parts.length - 1;\n out = parts.join(REDACTED);\n }\n }\n for (const rule of rules(mode)) {\n out = out.replace(rule.re, (match: string, ...args: unknown[]) => {\n const groups = args.slice(0, -2).map((g) => (typeof g === \"string\" ? g : \"\"));\n const replaced = rule.replace(match, ...groups);\n if (replaced === null || replaced === match) return match;\n count++;\n return replaced;\n });\n }\n return { text: out, count };\n}\n\nexport interface SecretLocation {\n line: number;\n kind: string;\n}\n\n/** Where precise-set secrets sit in a text (for lint_vault): 1-based line and kind, never the value. */\nexport function findSecrets(text: string): SecretLocation[] {\n knownSecrets ??= loadKnownSecrets();\n const hits: { index: number; kind: string }[] = [];\n for (const value of knownSecrets) {\n for (let i = text.indexOf(value); i !== -1; i = text.indexOf(value, i + value.length)) {\n hits.push({ index: i, kind: \"secret env value\" });\n }\n }\n for (const rule of rules(\"precise\")) {\n for (const m of text.matchAll(rule.re)) {\n const replaced = rule.replace(m[0], ...m.slice(1).map((g) => g ?? \"\"));\n if (replaced !== null && replaced !== m[0]) hits.push({ index: m.index ?? 0, kind: rule.name });\n }\n }\n const seen = new Set<string>();\n const out: SecretLocation[] = [];\n for (const h of hits.sort((a, b) => a.index - b.index)) {\n const line = text.slice(0, h.index).split(\"\\n\").length;\n const key = `${line}:${h.kind}`;\n if (!seen.has(key)) {\n seen.add(key);\n out.push({ line, kind: h.kind });\n }\n }\n return out;\n}\n\n/** Redact every string inside a JSON-like value (for log payloads and state files). */\nexport function redactDeep<T>(value: T, mode: RedactMode = \"broad\"): { value: T; count: number } {\n let count = 0;\n const walk = (v: unknown): unknown => {\n if (typeof v === \"string\") {\n const r = redact(v, mode);\n count += r.count;\n return r.text;\n }\n if (Array.isArray(v)) return v.map(walk);\n if (v && typeof v === \"object\") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));\n return v;\n };\n return { value: walk(value) as T, count };\n}\n\n/** Suffix for a tool result: says how many secrets were masked, never which. \"\" when none. */\nexport function redactedNote(count: number): string {\n return count > 0 ? ` (${count} secret${count === 1 ? \"\" : \"s\"} redacted)` : \"\";\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeDecay,
|
|
3
|
+
loadDecayConfig,
|
|
4
|
+
normalizeVerifiedDate
|
|
5
|
+
} from "./chunk-5ZKNO6ZD.js";
|
|
1
6
|
import {
|
|
2
7
|
readSelectionLog
|
|
3
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-NBY4TRP7.js";
|
|
4
9
|
import {
|
|
5
10
|
compileLexicon,
|
|
6
11
|
findAliasConflicts
|
|
7
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-FTRXH2WC.js";
|
|
8
13
|
import {
|
|
9
14
|
deriveProjectRoot
|
|
10
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-LGQPP5FQ.js";
|
|
16
|
+
import {
|
|
17
|
+
findSecrets
|
|
18
|
+
} from "./chunk-OEF5U5GV.js";
|
|
11
19
|
import {
|
|
12
20
|
loadSchema,
|
|
13
21
|
validateNote
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import {
|
|
16
|
-
computeDecay,
|
|
17
|
-
loadDecayConfig,
|
|
18
|
-
normalizeVerifiedDate
|
|
19
|
-
} from "./chunk-5ZKNO6ZD.js";
|
|
22
|
+
} from "./chunk-FURCIJBO.js";
|
|
20
23
|
|
|
21
24
|
// src/core/lint.ts
|
|
22
25
|
import { readFile } from "fs/promises";
|
|
@@ -46,6 +49,16 @@ async function lintVault(notesPath, opts = {}) {
|
|
|
46
49
|
findings.push(...validateNote(rel, raw, schema, { todayIso: opts.todayIso }));
|
|
47
50
|
findings.push(...findBrokenLinks(rel, raw, knownNames));
|
|
48
51
|
if (codeRoot) findings.push(...findCodeSymbolDrift(rel, raw, codeRoot));
|
|
52
|
+
if (opts.checkSecrets) {
|
|
53
|
+
for (const s of findSecrets(raw)) {
|
|
54
|
+
findings.push({
|
|
55
|
+
path: rel,
|
|
56
|
+
rule: "secrets",
|
|
57
|
+
severity: "error",
|
|
58
|
+
message: `line ${s.line}: ${s.kind} \u2014 rotate it if it is real, then remove it from the note (redaction does not rewrite git history)`
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
49
62
|
}
|
|
50
63
|
if (opts.checkDecayCandidates) {
|
|
51
64
|
findings.push(...await findDecayCandidates(notesPath, rawByPath, opts.todayIso));
|
|
@@ -71,7 +84,8 @@ async function lintVault(notesPath, opts = {}) {
|
|
|
71
84
|
code_symbols: findings.filter((f) => f.rule === "code_symbols"),
|
|
72
85
|
decay_candidates: findings.filter((f) => f.rule === "decay_candidates"),
|
|
73
86
|
alias_conflicts: findings.filter((f) => f.rule === "alias_conflicts"),
|
|
74
|
-
decoys: findings.filter((f) => f.rule === "decoys")
|
|
87
|
+
decoys: findings.filter((f) => f.rule === "decoys"),
|
|
88
|
+
...opts.checkSecrets ? { secrets: findings.filter((f) => f.rule === "secrets") } : {}
|
|
75
89
|
};
|
|
76
90
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
77
91
|
const warnings = findings.filter((f) => f.severity === "warn").length;
|
|
@@ -251,7 +265,7 @@ async function findDecoys(notesPath, rawByPath) {
|
|
|
251
265
|
return findings;
|
|
252
266
|
}
|
|
253
267
|
function formatLintReport(report, opts = {}) {
|
|
254
|
-
const target = opts.rule ? report.byRule[opts.rule] : report.findings;
|
|
268
|
+
const target = (opts.rule ? report.byRule[opts.rule] : report.findings) ?? [];
|
|
255
269
|
if (target.length === 0) {
|
|
256
270
|
return `clean: 0 findings across ${report.counts.filesLinted} files`;
|
|
257
271
|
}
|
|
@@ -269,4 +283,4 @@ export {
|
|
|
269
283
|
lintVault,
|
|
270
284
|
formatLintReport
|
|
271
285
|
};
|
|
272
|
-
//# sourceMappingURL=chunk-
|
|
286
|
+
//# sourceMappingURL=chunk-OFXQXVG5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/lint.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"gray-matter\";\nimport { loadSchema, validateNote, type LintFinding, type VaultSchema } from \"./schema.js\";\nimport { deriveProjectRoot } from \"./session.js\";\nimport { computeDecay, loadDecayConfig, normalizeVerifiedDate } from \"./decay.js\";\nimport { readSelectionLog } from \"./telemetry.js\";\nimport { compileLexicon, findAliasConflicts } from \"./lexicon.js\";\nimport { findSecrets } from \"./redact.js\";\n\nexport interface LintReport {\n schemaPath: string;\n findings: LintFinding[];\n byRule: {\n schema_violations: LintFinding[];\n missing_provenance: LintFinding[];\n stale: LintFinding[];\n broken_links: LintFinding[];\n code_symbols: LintFinding[];\n decay_candidates: LintFinding[];\n alias_conflicts: LintFinding[];\n decoys: LintFinding[];\n /** only present when the opt-in secrets check ran */\n secrets?: LintFinding[];\n };\n counts: {\n errors: number;\n warnings: number;\n filesLinted: number;\n };\n}\n\nexport async function lintVault(\n notesPath: string,\n opts: {\n pathGlob?: string;\n schemaOverride?: VaultSchema;\n todayIso?: string;\n /**\n * When true, run the code-symbol drift check: flag inline-code file-path\n * references in notes that no longer exist under the project root. Opt-in\n * (off by default) so the standard report and the healthcheck slow tier stay\n * quiet and byte-stable. Fails open when the project root is not a code repo.\n */\n checkCodeSymbols?: boolean;\n /** Override the project root for code-symbol resolution (default: derived from notesPath). */\n projectRoot?: string;\n /**\n * When true, run the decay-candidates check: cross-reference the selection\n * log (notes that appeared in recent search results) against each note's\n * current decay multiplier, and flag notes that are frequently retrieved yet\n * heavily decayed (\"relevant but going stale — verify or revise\"). Opt-in;\n * a silent no-op when no selection log exists.\n */\n checkDecayCandidates?: boolean;\n /**\n * When true, run the alias-conflict check: flag lexicon phrases that map to\n * more than one canonical target (a wrong binding misroutes queries). Opt-in;\n * no-op when the vault has no lexicon.\n */\n checkAliasConflicts?: boolean;\n /**\n * When true, run the decoys check: flag notes that were retrieved often (per\n * the selection log) but NEVER cited in an answer. Surfaced for a human to\n * judge — a decoy might be genuine noise OR a note the answers were wrong to\n * skip; usage-feedback ranking (v1.5) never auto-down-ranks on this ambiguous\n * signal (decision Q5). Opt-in; a no-op with no selection log.\n */\n checkDecoys?: boolean;\n /**\n * When true, run the secrets check (v1.5.3): flag notes that contain a secret\n * (API key, token, password, private key — the redactor's precise set). Reports\n * path, line and kind, never the value; nothing is rewritten. Opt-in, so the\n * default report stays byte-stable. A real secret must be rotated: it is in\n * git history if the note was ever committed.\n */\n checkSecrets?: boolean;\n } = {}\n): Promise<LintReport> {\n const schema = opts.schemaOverride ?? (await loadSchema(notesPath));\n const files = await glob(opts.pathGlob ?? \"**/*.md\", {\n cwd: notesPath,\n ignore: [\"node_modules/**\", \".semantic-sidekick-index/**\", \"vault.schema.yml\"],\n });\n\n const findings: LintFinding[] = [];\n // Build known-note set first, to make broken-link detection O(1) per link.\n const knownNames = new Set<string>();\n const rawByPath = new Map<string, string>();\n for (const rel of files) {\n try {\n const raw = await readFile(join(notesPath, rel), \"utf-8\");\n rawByPath.set(rel, raw);\n // Wikilink names match on basename-without-ext (obsidian-style)\n knownNames.add(basename(rel).replace(/\\.md$/, \"\"));\n } catch {\n /* skip */\n }\n }\n\n // Code-symbol drift is opt-in and only meaningful against a real code repo.\n const codeRoot = opts.checkCodeSymbols\n ? resolveCodeRoot(opts.projectRoot ?? deriveProjectRoot(notesPath))\n : null;\n\n for (const [rel, raw] of rawByPath) {\n findings.push(...validateNote(rel, raw, schema, { todayIso: opts.todayIso }));\n findings.push(...findBrokenLinks(rel, raw, knownNames));\n if (codeRoot) findings.push(...findCodeSymbolDrift(rel, raw, codeRoot));\n if (opts.checkSecrets) {\n for (const s of findSecrets(raw)) {\n findings.push({\n path: rel,\n rule: \"secrets\",\n severity: \"error\",\n message: `line ${s.line}: ${s.kind} — rotate it if it is real, then remove it from the note (redaction does not rewrite git history)`,\n });\n }\n }\n }\n\n if (opts.checkDecayCandidates) {\n findings.push(...(await findDecayCandidates(notesPath, rawByPath, opts.todayIso)));\n }\n\n if (opts.checkAliasConflicts) {\n for (const c of findAliasConflicts(await compileLexicon(notesPath))) {\n findings.push({\n path: \"lexicon\",\n rule: \"alias_conflicts\",\n severity: \"warn\",\n message: `phrase \"${c.phrase}\" maps to ${c.canonicals.length} targets: ${c.canonicals.join(\", \")} — pick one`,\n });\n }\n }\n\n if (opts.checkDecoys) {\n findings.push(...(await findDecoys(notesPath, rawByPath)));\n }\n\n const byRule = {\n schema_violations: findings.filter((f) => f.rule === \"schema_violations\"),\n missing_provenance: findings.filter((f) => f.rule === \"missing_provenance\"),\n stale: findings.filter((f) => f.rule === \"stale\"),\n broken_links: findings.filter((f) => f.rule === \"broken_links\"),\n code_symbols: findings.filter((f) => f.rule === \"code_symbols\"),\n decay_candidates: findings.filter((f) => f.rule === \"decay_candidates\"),\n alias_conflicts: findings.filter((f) => f.rule === \"alias_conflicts\"),\n decoys: findings.filter((f) => f.rule === \"decoys\"),\n ...(opts.checkSecrets ? { secrets: findings.filter((f) => f.rule === \"secrets\") } : {}),\n };\n const errors = findings.filter((f) => f.severity === \"error\").length;\n const warnings = findings.filter((f) => f.severity === \"warn\").length;\n\n return {\n schemaPath: join(notesPath, \"vault.schema.yml\"),\n findings,\n byRule,\n counts: { errors, warnings, filesLinted: files.length },\n };\n}\n\nfunction findBrokenLinks(\n relPath: string,\n rawFileContent: string,\n knownNames: Set<string>\n): LintFinding[] {\n const { content } = matter(rawFileContent);\n const findings: LintFinding[] = [];\n // Match [[target]] and [[target|alias]] — skip fenced code and inline code.\n const stripped = content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]+`/g, \"\");\n const re = /\\[\\[([^\\]\\|]+)(?:\\|[^\\]]+)?\\]\\]/g;\n const seen = new Set<string>();\n let m: RegExpExecArray | null;\n while ((m = re.exec(stripped)) !== null) {\n const target = m[1].trim().split(\"#\")[0].split(\"/\").pop()!.replace(/\\.md$/, \"\");\n if (seen.has(target)) continue;\n seen.add(target);\n if (!knownNames.has(target)) {\n findings.push({\n path: relPath,\n rule: \"broken_links\",\n severity: \"warn\",\n message: `wikilink [[${target}]] has no matching note`,\n });\n }\n }\n return findings;\n}\n\n/** File extensions we treat as \"this looks like a source/doc file reference\". */\nconst CODE_EXTENSIONS = new Set([\n \"ts\", \"tsx\", \"js\", \"jsx\", \"mjs\", \"cjs\", \"py\", \"rb\", \"go\", \"rs\", \"java\", \"kt\",\n \"c\", \"h\", \"cpp\", \"hpp\", \"cs\", \"php\", \"swift\", \"scala\", \"sh\", \"bash\", \"sql\",\n \"json\", \"yaml\", \"yml\", \"toml\", \"css\", \"scss\", \"html\", \"vue\", \"svelte\", \"md\",\n]);\n\n/**\n * Decide whether `root` is a real code repository worth checking paths against.\n * Fails open (returns null) for non-code roots so the check is a silent no-op\n * outside a project — e.g. a standalone vault, or when babel-fish isn't present.\n */\nfunction resolveCodeRoot(root: string): string | null {\n if (!root || !existsSync(root)) return null;\n const markers = [\"package.json\", \".git\", \"src\", \"lib\", \"pyproject.toml\", \"go.mod\", \"Cargo.toml\"];\n return markers.some((m) => existsSync(join(root, m))) ? root : null;\n}\n\n/**\n * Code-symbol drift: scan a note's inline-code spans for repo-relative file-path\n * references and flag ones whose first path segment IS a real directory in the repo\n * but whose full path no longer exists — i.e. a stale reference to a moved/deleted\n * file. Anchoring on an existing first segment keeps false positives low: paths\n * belonging to other repos (whose top-level dir isn't present here) are skipped.\n *\n * Scope note: this validates *path* references. Fine-grained symbol-name checking\n * (function/class identifiers) needs a real symbol index and is planned for the\n * v1.4 lexicon arc, which will extend this same `code_symbols` rule.\n */\nfunction findCodeSymbolDrift(relPath: string, rawFileContent: string, codeRoot: string): LintFinding[] {\n const { content } = matter(rawFileContent);\n // Inline-code spans only (single backtick). Fenced blocks are illustrative and\n // over-match. Strip fenced blocks first so their backticks don't confuse the span regex.\n const withoutFences = content.replace(/```[\\s\\S]*?```/g, \"\");\n const findings: LintFinding[] = [];\n const seen = new Set<string>();\n const re = /`([^`\\n]+)`/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(withoutFences)) !== null) {\n let token = m[1].trim();\n if (!looksLikeRepoPath(token)) continue;\n token = token.replace(/^\\.\\//, \"\");\n if (seen.has(token)) continue;\n seen.add(token);\n const firstSeg = token.split(\"/\")[0];\n // Only judge paths anchored in a directory that actually exists in this repo.\n if (!existsSync(join(codeRoot, firstSeg))) continue;\n if (!existsSync(join(codeRoot, token))) {\n findings.push({\n path: relPath,\n rule: \"code_symbols\",\n severity: \"warn\",\n message: `inline code path \\`${token}\\` does not exist under the project root (stale reference?)`,\n });\n }\n }\n return findings;\n}\n\nfunction looksLikeRepoPath(token: string): boolean {\n if (!token.includes(\"/\")) return false;\n if (/\\s/.test(token)) return false; // commands, prose\n if (token.includes(\"://\")) return false; // URLs\n if (/[*?\\[\\]{}()<>|]/.test(token)) return false; // globs / shell / placeholders\n if (token.startsWith(\"/\") || token.startsWith(\"~\")) return false; // absolute / home\n const normalized = token.replace(/^\\.\\//, \"\");\n if (normalized.startsWith(\"..\")) return false; // escapes the repo\n const last = normalized.split(\"/\").pop() ?? \"\";\n const dot = last.lastIndexOf(\".\");\n if (dot <= 0) return false; // no extension → likely a dir or a symbol like a/b\n const ext = last.slice(dot + 1).toLowerCase();\n return CODE_EXTENSIONS.has(ext);\n}\n\n/** Multiplier at or below which a frequently-retrieved note is worth surfacing. */\nconst DECAY_CANDIDATE_THRESHOLD = 0.5;\n\n/**\n * Decay-candidates: notes that keep showing up in search results but have decayed.\n * Reads the selection log (search events record the paths each query returned),\n * computes each retrieved note's current decay multiplier from its frontmatter,\n * and flags those at/below the threshold — \"this keeps being relevant but is\n * going stale; verify or revise.\" Index-free (no embedder); a no-op when there's\n * no selection log yet.\n */\nasync function findDecayCandidates(\n notesPath: string,\n rawByPath: Map<string, string>,\n todayIso?: string\n): Promise<LintFinding[]> {\n const events = await readSelectionLog(notesPath);\n if (events.length === 0) return [];\n\n // Count how often each path was retrieved (appeared in a search's results).\n const retrieved = new Map<string, number>();\n for (const e of events) {\n if (e.kind === \"search\") {\n for (const r of e.results) retrieved.set(r.path, (retrieved.get(r.path) ?? 0) + 1);\n }\n }\n if (retrieved.size === 0) return [];\n\n const config = loadDecayConfig(notesPath);\n if (!config.enabled) return [];\n const nowMs = todayIso ? Date.parse(todayIso) : undefined;\n\n const findings: LintFinding[] = [];\n for (const [rel, count] of retrieved) {\n const raw = rawByPath.get(rel);\n if (!raw) continue; // retrieved path no longer in the vault — skip\n const { data: fm } = matter(raw);\n const decay = computeDecay({\n type: typeof fm.type === \"string\" ? fm.type : undefined,\n last_verified: normalizeVerifiedDate(fm.last_verified),\n evergreen: fm.evergreen === true,\n config,\n nowMs,\n });\n if (decay.multiplier <= DECAY_CANDIDATE_THRESHOLD) {\n findings.push({\n path: rel,\n rule: \"decay_candidates\",\n severity: \"warn\",\n message: `retrieved ${count}× recently but decayed to ${decay.multiplier.toFixed(2)} (age ${Math.round(decay.age_days)}d) — verify_note or revise`,\n });\n }\n }\n // Most-retrieved first — the ones worth re-verifying soonest.\n findings.sort((a, b) => (retrieved.get(b.path) ?? 0) - (retrieved.get(a.path) ?? 0));\n return findings;\n}\n\n/** A note must be retrieved at least this many times before \"never cited\" is signal. */\nconst DECOY_MIN_RETRIEVALS = 3;\n\n/**\n * Decoys (v1.5 Phase 9): notes retrieved ≥ DECOY_MIN_RETRIEVALS times that were\n * NEVER cited. Surfaced only — usage-feedback ranking never auto-down-ranks on\n * this ambiguous signal (a decoy might be genuine noise, or a note the answers\n * were wrong to skip). A human decides; the lint just points.\n */\nasync function findDecoys(notesPath: string, rawByPath: Map<string, string>): Promise<LintFinding[]> {\n const events = await readSelectionLog(notesPath);\n if (events.length === 0) return [];\n\n const retrieved = new Map<string, number>();\n const cited = new Set<string>();\n for (const e of events) {\n if (e.kind === \"search\") {\n for (const r of e.results) retrieved.set(r.path, (retrieved.get(r.path) ?? 0) + 1);\n } else if (e.kind === \"selection\") {\n cited.add(e.note_path);\n }\n }\n\n const findings: LintFinding[] = [];\n for (const [rel, count] of retrieved) {\n if (count < DECOY_MIN_RETRIEVALS) continue;\n if (cited.has(rel)) continue;\n if (!rawByPath.has(rel)) continue; // retrieved path no longer in the vault — skip\n findings.push({\n path: rel,\n rule: \"decoys\",\n severity: \"warn\",\n message: `retrieved ${count}× but never cited — decoy noise, or a note answers wrongly skip? (surfaced, not auto-down-ranked)`,\n });\n }\n findings.sort((a, b) => (retrieved.get(b.path) ?? 0) - (retrieved.get(a.path) ?? 0));\n return findings;\n}\n\nexport function formatLintReport(report: LintReport, opts: { rule?: keyof LintReport[\"byRule\"] } = {}): string {\n const target = (opts.rule ? report.byRule[opts.rule] : report.findings) ?? [];\n if (target.length === 0) {\n return `clean: 0 findings across ${report.counts.filesLinted} files`;\n }\n const lines = [\n `${target.length} finding${target.length === 1 ? \"\" : \"s\"} (errors=${report.counts.errors}, warnings=${report.counts.warnings}, files=${report.counts.filesLinted})`,\n \"\",\n ];\n for (const f of target) {\n lines.push(`[${f.severity}] ${f.rule}: ${f.path} — ${f.message}`);\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
redact,
|
|
3
|
+
redactDeep
|
|
4
|
+
} from "./chunk-OEF5U5GV.js";
|
|
1
5
|
import {
|
|
2
6
|
loadSchema,
|
|
3
7
|
validateNote
|
|
4
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-FURCIJBO.js";
|
|
5
9
|
|
|
6
10
|
// src/core/crud.ts
|
|
7
11
|
import { readFile, writeFile, unlink, rename, mkdir } from "fs/promises";
|
|
@@ -20,11 +24,13 @@ var NoteCrud = class {
|
|
|
20
24
|
throw new Error(`Note already exists: ${relativePath}`);
|
|
21
25
|
}
|
|
22
26
|
await mkdir(dirname(absPath), { recursive: true });
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
27
|
+
const body = redact(content);
|
|
28
|
+
const fm = redactDeep(frontmatter ?? {}, "precise");
|
|
29
|
+
const stamps = computeStamps(body.text);
|
|
30
|
+
const merged = { ...stamps, ...fm.value };
|
|
31
|
+
const fileContent = matter.stringify(body.text, merged);
|
|
26
32
|
await writeFile(absPath, fileContent, "utf-8");
|
|
27
|
-
return relativePath;
|
|
33
|
+
return { path: relativePath, redacted: body.count + fm.count };
|
|
28
34
|
}
|
|
29
35
|
async read(relativePath) {
|
|
30
36
|
const absPath = join(this.notesPath, relativePath);
|
|
@@ -50,6 +56,8 @@ var NoteCrud = class {
|
|
|
50
56
|
throw new Error(`Note does not exist: ${relativePath}`);
|
|
51
57
|
}
|
|
52
58
|
const existing = await readFile(absPath, "utf-8");
|
|
59
|
+
const incoming = redact(content);
|
|
60
|
+
content = incoming.text;
|
|
53
61
|
let updated;
|
|
54
62
|
switch (options.mode) {
|
|
55
63
|
case "overwrite":
|
|
@@ -74,6 +82,7 @@ var NoteCrud = class {
|
|
|
74
82
|
const stamps = computeStamps(body);
|
|
75
83
|
const newFm = { ...existingFm, ...stamps };
|
|
76
84
|
await writeFile(absPath, matter.stringify(body, newFm), "utf-8");
|
|
85
|
+
return { path: relativePath, redacted: incoming.count };
|
|
77
86
|
}
|
|
78
87
|
async delete(relativePath) {
|
|
79
88
|
const absPath = join(this.notesPath, relativePath);
|
|
@@ -172,7 +181,8 @@ var FrontmatterManager = class {
|
|
|
172
181
|
const absPath = join2(this.notesPath, relativePath);
|
|
173
182
|
const raw = await readFile2(absPath, "utf-8");
|
|
174
183
|
const { data, content } = matter2(raw);
|
|
175
|
-
|
|
184
|
+
const incoming = redactDeep(fields, "precise");
|
|
185
|
+
for (const [key, value] of Object.entries(incoming.value)) {
|
|
176
186
|
if (value === null || value === void 0) {
|
|
177
187
|
delete data[key];
|
|
178
188
|
} else {
|
|
@@ -181,6 +191,7 @@ var FrontmatterManager = class {
|
|
|
181
191
|
}
|
|
182
192
|
const updated = matter2.stringify(content, data);
|
|
183
193
|
await writeFile2(absPath, updated, "utf-8");
|
|
194
|
+
return { redacted: incoming.count };
|
|
184
195
|
}
|
|
185
196
|
};
|
|
186
197
|
var TagManager = class {
|
|
@@ -257,6 +268,8 @@ function escapeRegex2(str) {
|
|
|
257
268
|
// src/core/watcher.ts
|
|
258
269
|
import { watch } from "chokidar";
|
|
259
270
|
import { EventEmitter } from "events";
|
|
271
|
+
var IGNORED_DIRS = /(^|[\\/])(node_modules|\.semantic-sidekick-index|\.git)([\\/]|$)/;
|
|
272
|
+
var isMarkdown = (path) => path.endsWith(".md");
|
|
260
273
|
var Watcher = class extends EventEmitter {
|
|
261
274
|
notesPath;
|
|
262
275
|
fsWatcher = null;
|
|
@@ -273,15 +286,11 @@ var Watcher = class extends EventEmitter {
|
|
|
273
286
|
}
|
|
274
287
|
start() {
|
|
275
288
|
if (this.fsWatcher) return;
|
|
276
|
-
this.fsWatcher = watch("
|
|
289
|
+
this.fsWatcher = watch(".", {
|
|
277
290
|
cwd: this.notesPath,
|
|
278
291
|
ignoreInitial: true,
|
|
279
292
|
followSymlinks: true,
|
|
280
|
-
ignored:
|
|
281
|
-
"**/node_modules/**",
|
|
282
|
-
"**/.semantic-sidekick-index/**",
|
|
283
|
-
"**/.git/**"
|
|
284
|
-
],
|
|
293
|
+
ignored: (path, stats) => IGNORED_DIRS.test(path) || stats?.isFile() === true && !isMarkdown(path),
|
|
285
294
|
...this.usePolling ? { usePolling: true, interval: 100 } : {}
|
|
286
295
|
});
|
|
287
296
|
this.readyPromise = new Promise((resolve) => {
|
|
@@ -302,6 +311,7 @@ var Watcher = class extends EventEmitter {
|
|
|
302
311
|
this.pendingChanges.clear();
|
|
303
312
|
}
|
|
304
313
|
enqueue(path) {
|
|
314
|
+
if (!isMarkdown(path)) return;
|
|
305
315
|
this.pendingChanges.add(path);
|
|
306
316
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
307
317
|
this.debounceTimer = setTimeout(() => {
|
|
@@ -561,16 +571,16 @@ async function applyPatch(notesPath, changeset, options = {}) {
|
|
|
561
571
|
try {
|
|
562
572
|
for (const c of changeset.creates ?? []) {
|
|
563
573
|
const abs = join4(notesPath, c.path);
|
|
564
|
-
await crud.create(c.path, c.content, c.frontmatter);
|
|
574
|
+
const { redacted } = await crud.create(c.path, c.content, c.frontmatter);
|
|
565
575
|
snapshots.push({ op: "create", path: c.path, absPath: abs });
|
|
566
|
-
applied.push({ op: "create", path: c.path });
|
|
576
|
+
applied.push({ op: "create", path: c.path, ...redacted ? { redacted } : {} });
|
|
567
577
|
}
|
|
568
578
|
for (const u of changeset.updates ?? []) {
|
|
569
579
|
const abs = join4(notesPath, u.path);
|
|
570
580
|
const prev = await readFile4(abs, "utf-8");
|
|
571
|
-
await crud.update(u.path, u.content, { mode: u.mode, heading: u.heading });
|
|
581
|
+
const { redacted } = await crud.update(u.path, u.content, { mode: u.mode, heading: u.heading });
|
|
572
582
|
snapshots.push({ op: "update", path: u.path, absPath: abs, prevContent: prev });
|
|
573
|
-
applied.push({ op: "update", path: u.path });
|
|
583
|
+
applied.push({ op: "update", path: u.path, ...redacted ? { redacted } : {} });
|
|
574
584
|
}
|
|
575
585
|
for (const m of changeset.moves ?? []) {
|
|
576
586
|
await crud.move(m.from, m.to);
|
|
@@ -934,4 +944,4 @@ export {
|
|
|
934
944
|
regenerateAgentsContract,
|
|
935
945
|
inspectAgentsContract
|
|
936
946
|
};
|
|
937
|
-
//# sourceMappingURL=chunk-
|
|
947
|
+
//# sourceMappingURL=chunk-ROKSUJFM.js.map
|