@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.
Files changed (75) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +43 -0
  4. package/README.md +4 -0
  5. package/dist/{chunk-25EIQWXL.js → chunk-44BQDRXD.js} +5 -5
  6. package/dist/chunk-44BQDRXD.js.map +1 -0
  7. package/dist/{chunk-2EELGTLX.js → chunk-D5QLNNA4.js} +2 -2
  8. package/dist/{chunk-A3KYHKB5.js → chunk-FTRXH2WC.js} +8 -4
  9. package/dist/chunk-FTRXH2WC.js.map +1 -0
  10. package/dist/{chunk-OWZWMB4P.js → chunk-FURCIJBO.js} +1 -1
  11. package/dist/chunk-FURCIJBO.js.map +1 -0
  12. package/dist/{chunk-V45NJIHI.js → chunk-IGHCVAAL.js} +19 -4
  13. package/dist/chunk-IGHCVAAL.js.map +1 -0
  14. package/dist/{chunk-S4KW7VH4.js → chunk-LGQPP5FQ.js} +8 -3
  15. package/dist/chunk-LGQPP5FQ.js.map +1 -0
  16. package/dist/{chunk-NTYYS53O.js → chunk-NBY4TRP7.js} +7 -3
  17. package/dist/chunk-NBY4TRP7.js.map +1 -0
  18. package/dist/chunk-OEF5U5GV.js +175 -0
  19. package/dist/chunk-OEF5U5GV.js.map +1 -0
  20. package/dist/{chunk-FM26NFTR.js → chunk-OFXQXVG5.js} +26 -12
  21. package/dist/chunk-OFXQXVG5.js.map +1 -0
  22. package/dist/{chunk-CSXZ7QUU.js → chunk-ROKSUJFM.js} +27 -17
  23. package/dist/chunk-ROKSUJFM.js.map +1 -0
  24. package/dist/{chunk-JPQHNG3R.js → chunk-SX427M2R.js} +10 -4
  25. package/dist/chunk-SX427M2R.js.map +1 -0
  26. package/dist/{chunk-7M3JZOYH.js → chunk-WZEKIUYG.js} +8 -4
  27. package/dist/chunk-WZEKIUYG.js.map +1 -0
  28. package/dist/{chunk-WSU4QIPB.js → chunk-XAXK7NM4.js} +14 -8
  29. package/dist/chunk-XAXK7NM4.js.map +1 -0
  30. package/dist/cli/index.js +15 -15
  31. package/dist/core/index.d.ts +24 -4
  32. package/dist/core/index.js +20 -12
  33. package/dist/core/redact.d.ts +36 -0
  34. package/dist/core/redact.js +18 -0
  35. package/dist/{dossier-2UG724LB.js → dossier-RWTIMGZN.js} +4 -3
  36. package/dist/healthcheck-A32IWPNV.js +20 -0
  37. package/dist/indexer-RAKJ7JR3.js +8 -0
  38. package/dist/{lexicon-5PXQEMUF.js → lexicon-IVNJTMWE.js} +5 -4
  39. package/dist/lint-HURYGO3V.js +17 -0
  40. package/dist/log-YIXF6CYM.js +11 -0
  41. package/dist/mcp/server.js +32 -27
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/{profile-LXUEF7TX.js → profile-UE4YVTDC.js} +5 -4
  44. package/dist/{schema-ATFCLC6Y.js → schema-LY6533NJ.js} +2 -2
  45. package/dist/{search-AHKEFI2B.js → search-ZS6OW74K.js} +3 -3
  46. package/dist/{telemetry-GB2TEHVE.js → telemetry-B4KP3UVE.js} +4 -3
  47. package/dist/telemetry-B4KP3UVE.js.map +1 -0
  48. package/hooks/vault-context.js +112 -58
  49. package/package.json +10 -10
  50. package/dist/chunk-25EIQWXL.js.map +0 -1
  51. package/dist/chunk-7M3JZOYH.js.map +0 -1
  52. package/dist/chunk-A3KYHKB5.js.map +0 -1
  53. package/dist/chunk-CSXZ7QUU.js.map +0 -1
  54. package/dist/chunk-FM26NFTR.js.map +0 -1
  55. package/dist/chunk-JPQHNG3R.js.map +0 -1
  56. package/dist/chunk-NTYYS53O.js.map +0 -1
  57. package/dist/chunk-OWZWMB4P.js.map +0 -1
  58. package/dist/chunk-S4KW7VH4.js.map +0 -1
  59. package/dist/chunk-V45NJIHI.js.map +0 -1
  60. package/dist/chunk-WSU4QIPB.js.map +0 -1
  61. package/dist/healthcheck-IMB47LZE.js +0 -19
  62. package/dist/indexer-YW4O3VVM.js +0 -8
  63. package/dist/lint-XUQ7JDYX.js +0 -16
  64. package/dist/log-LQXIJXIA.js +0 -10
  65. /package/dist/{chunk-2EELGTLX.js.map → chunk-D5QLNNA4.js.map} +0 -0
  66. /package/dist/{dossier-2UG724LB.js.map → core/redact.js.map} +0 -0
  67. /package/dist/{healthcheck-IMB47LZE.js.map → dossier-RWTIMGZN.js.map} +0 -0
  68. /package/dist/{indexer-YW4O3VVM.js.map → healthcheck-A32IWPNV.js.map} +0 -0
  69. /package/dist/{lexicon-5PXQEMUF.js.map → indexer-RAKJ7JR3.js.map} +0 -0
  70. /package/dist/{lint-XUQ7JDYX.js.map → lexicon-IVNJTMWE.js.map} +0 -0
  71. /package/dist/{log-LQXIJXIA.js.map → lint-HURYGO3V.js.map} +0 -0
  72. /package/dist/{profile-LXUEF7TX.js.map → log-YIXF6CYM.js.map} +0 -0
  73. /package/dist/{schema-ATFCLC6Y.js.map → profile-UE4YVTDC.js.map} +0 -0
  74. /package/dist/{telemetry-GB2TEHVE.js.map → schema-LY6533NJ.js.map} +0 -0
  75. /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-NTYYS53O.js";
8
+ } from "./chunk-NBY4TRP7.js";
4
9
  import {
5
10
  compileLexicon,
6
11
  findAliasConflicts
7
- } from "./chunk-A3KYHKB5.js";
12
+ } from "./chunk-FTRXH2WC.js";
8
13
  import {
9
14
  deriveProjectRoot
10
- } from "./chunk-S4KW7VH4.js";
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-OWZWMB4P.js";
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-FM26NFTR.js.map
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-OWZWMB4P.js";
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 stamps = computeStamps(content);
24
- const merged = { ...stamps, ...frontmatter ?? {} };
25
- const fileContent = matter.stringify(content, merged);
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
- for (const [key, value] of Object.entries(fields)) {
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("**/*.md", {
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-CSXZ7QUU.js.map
947
+ //# sourceMappingURL=chunk-ROKSUJFM.js.map