@theglitchking/semantic-memory 1.5.2 → 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 +22 -0
- package/README.md +4 -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-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-JWPR2B2X.js → chunk-ROKSUJFM.js} +22 -11
- 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 +13 -13
- package/dist/core/index.d.ts +24 -4
- package/dist/core/index.js +18 -10
- 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/{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 +29 -24
- 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/{telemetry-GB2TEHVE.js → telemetry-B4KP3UVE.js} +4 -3
- package/dist/telemetry-B4KP3UVE.js.map +1 -0
- package/hooks/vault-context.js +17 -3
- package/package.json +1 -1
- package/dist/chunk-7M3JZOYH.js.map +0 -1
- package/dist/chunk-A3KYHKB5.js.map +0 -1
- package/dist/chunk-FM26NFTR.js.map +0 -1
- package/dist/chunk-JPQHNG3R.js.map +0 -1
- package/dist/chunk-JWPR2B2X.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-WSU4QIPB.js.map +0 -1
- package/dist/healthcheck-IMB47LZE.js +0 -19
- 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/{lexicon-5PXQEMUF.js.map → healthcheck-A32IWPNV.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
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Single-plugin marketplace for semantic-memory — the unified memory-layer plugin. v1.1.0 absorbs brain's strengths: AGENTS.md contract artifact, hard-gated verification sessions, multi-agent skill bundler (codex/copilot/pi), and SessionStart drift detection. v1.3 adds confidence decay — age-aware retrieval ranking with verify_note to reset the clock. All backwards-compatible with v1.0.x via deprecation shims.",
|
|
9
|
-
"version": "1.5.
|
|
9
|
+
"version": "1.5.3"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "semantic-memory",
|
|
14
14
|
"description": "Unified memory-layer plugin for Claude Code. One MCP server, 44 tools across multi-corpus search/read/write/graph/lint/sessions/contract. v1.1.0 adds: AGENTS.md contract artifact, hard-gated verification sessions (session_start/run/finish/status), proposal-mode synthesize_note + synthesize_promote, multi-agent skill bundler (codex/copilot/pi via the new `skills` CLI), and SessionStart drift detection. v1.2 completes the hygiene line: `/healthcheck --fix` auto-remediation, `lint_vault({checks:['code_symbols']})` code-path drift, and a fresh-install CI smoke gate. v1.3 adds confidence decay: age-aware search ranking (0.5^(age/half-life), type-aware, floored, composes with load_priority), a `decay` block surfaced on decayed results, `verify_note` to reset the clock, `evergreen` opt-out, and `decay-config`/`decay-trace` CLI introspection. v1.3.1 adds local-only selection-logging telemetry (which note each search result actually got used), the `decay_candidates` lint, and a `selection-stats` CLI. v1.4.0 is the resident-expert bridge: a learned human→artifact lexicon (`manage_lexicon`) with propose-and-confirm alias capture, verbatim symptom capture + symptom-keyed indexing (terse symptom queries match the note), Tier-1 lexicon query expansion in the hook, archive path-class down-weight, injection hygiene (score-gate + dedup + top-3 + compact instructions), section-targeted `read_note`, and opt-in conditional tool registration. v1.5.0 is the expert-character layer: entity dossiers (`manage_dossier`) — per-component living notes (purpose / failure modes / knobs / incident log / current state) that accrete in place, with two-hop retrieval (utterance→entity→dossier injected first); usage-feedback ranking that boosts repeatedly-cited notes (capped, composes with decay/path-class) plus a `decoys` lint for retrieved-but-never-cited notes (surfaced, never auto-down-ranked); session paging (Stop drafts a durable session digest as a proposal, SessionStart pages in a curated digest instead of a broad sweep); and a speaker profile (`manage_profile`) that models how the human communicates — severity calibration, chronic omissions, verbosity, shorthand — injected at SessionStart. Backwards-compatible with v1.0.x — every previous tool name remains callable through deprecation shims (removed in v2.0.0).",
|
|
15
|
-
"version": "1.5.
|
|
15
|
+
"version": "1.5.3",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TheGlitchKing"
|
|
18
18
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semantic-memory",
|
|
3
3
|
"description": "Unified memory-layer plugin for Claude Code. One MCP server, multiple typed corpora (vault / code / plans / docs / research / project-map), unified cross-corpus knowledge graph, conditional tool registration, in-process drift detection, and human↔code translation. Rebrand of `semantic-sidekick` at v1.0.0 — see CHANGELOG for migration.",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TheGlitchKing",
|
|
7
7
|
"email": "theglitchking@users.noreply.github.com"
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to semantic-memory (formerly semantic-sidekick) will be documented here.
|
|
4
4
|
|
|
5
|
+
## [1.5.3] - 2026-10-24 — Secret redaction
|
|
6
|
+
|
|
7
|
+
Patch release. Until now nothing stopped an API key, token or password that passed through the plugin from being written to disk in the clear — and vault notes are usually committed to git. Every write point now masks secrets first, and `lint_vault` can find the ones already there.
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- **Redaction at every write point** (`src/core/redact.ts`), two layers: the exact values of secret-named env vars on the machine (process env + `~/.claude/settings.json` `env`), then patterns — PEM private keys, `user:pass@` URL credentials, `Bearer`/`Basic` headers, secret-named `name=value` / `name: value` / JSON pairs, `--token VALUE` flags, vendor key prefixes (`sk-`/`pk-`/`rk-`, `ghp_`, `github_pat_`, `glpat-`, `xox*-`, `GOCSPX-`, `hvs.`, `AKIA`/`ASIA`, `AIza`, `whsec_`, `re_`, `hf_`, `npm_`), JWTs.
|
|
12
|
+
- **Notes** (create/update, `apply_patch`, synthesize, ingest, promote, `update_frontmatter`, dossiers, speaker profile, lexicon aliases, `log.md`) use a *precise* set tuned so ordinary content survives: commit SHAs, UUIDs, hashes, paths, `pk_customer_orders`, `max_tokens`, `monkey: …`, code such as `api_key = os.getenv` (a name=value only counts when the value looks random: a digit, or 16+ letters) and placeholders (`<your-key>`, `${API_TOKEN}`, `process.env.X`, `changeme`, `xxxx`) are untouched. `update_note` redacts only the incoming text.
|
|
13
|
+
- **Logs and state** (search queries in `selection.jsonl`, `session.json` task / `session_run` command and output tail, the hook's capture-pending excerpt) also mask long base64 and letter-and-digit tokens.
|
|
14
|
+
- The hook imports the redactor from `dist/core/redact.js`; if it cannot load, the capture excerpt is withheld rather than stored raw.
|
|
15
|
+
- Tool results say how many secrets were redacted (`… (2 secrets redacted)`, or `redacted: N` in JSON results), never which.
|
|
16
|
+
- **`lint_vault({ checks: ["secrets"] })`** — opt-in check that reports notes already containing a secret: path, line and kind, never the value; nothing is rewritten. A secret that was ever committed must be **rotated**: redaction cannot remove it from git history.
|
|
17
|
+
- Known limit: a secret written as plain prose ("the password is hunter2") is not caught.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `NoteCrud.create()` returns `{ path, redacted }` instead of the path string; `NoteCrud.update()` returns the same shape (was `void`). Only affects code using `@theglitchking/semantic-memory` as a library.
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
|
|
25
|
+
- Full suite **516 green** (440 in 1.5.2). One integration test per write point through the real MCP tools (all fail without the change), a sweep of the whole vault for the fake values, false-positive cases for ordinary note content, linear-time checks on 1 MB pathological inputs.
|
|
26
|
+
|
|
5
27
|
## [1.5.2] - 2026-10-24 — Docs index freshness (issue #29)
|
|
6
28
|
|
|
7
29
|
Patch release. In the standard two-server setup (`semantic-vault` → `.claude/.vault`, `semantic-sidekick` → `.documentation --read-only`) the docs index went stale after it was first built and the hooks never searched the docs. Fixes for every cause found, plus the dependency audit.
|
package/README.md
CHANGED
|
@@ -78,6 +78,10 @@ System (2): `get_stats`, `reindex`
|
|
|
78
78
|
|
|
79
79
|
See the originating design discussion in `.planning/semantic-sidekick/` for rationale and phase-by-phase build plan.
|
|
80
80
|
|
|
81
|
+
## Privacy and secrets
|
|
82
|
+
|
|
83
|
+
Everything stays on your machine (local embedding model, no network telemetry). Since v1.5.3 every write masks API keys, tokens, passwords and private keys before they reach disk, and `lint_vault({ checks: ["secrets"] })` finds ones already in notes. Details and limits: [SECURITY.md](SECURITY.md#secret-redaction-v153).
|
|
84
|
+
|
|
81
85
|
## Development
|
|
82
86
|
|
|
83
87
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
lintVault
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-OFXQXVG5.js";
|
|
4
4
|
|
|
5
5
|
// src/core/healthcheck.ts
|
|
6
6
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
@@ -338,4 +338,4 @@ export {
|
|
|
338
338
|
filterToDrift,
|
|
339
339
|
formatDriftBanner
|
|
340
340
|
};
|
|
341
|
-
//# sourceMappingURL=chunk-
|
|
341
|
+
//# sourceMappingURL=chunk-D5QLNNA4.js.map
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listDossiers
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-XAXK7NM4.js";
|
|
4
4
|
import {
|
|
5
5
|
deriveSessionDir
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-LGQPP5FQ.js";
|
|
7
|
+
import {
|
|
8
|
+
redactDeep
|
|
9
|
+
} from "./chunk-OEF5U5GV.js";
|
|
7
10
|
|
|
8
11
|
// src/core/lexicon.ts
|
|
9
12
|
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
@@ -88,7 +91,8 @@ function expandQuery(aliases, query) {
|
|
|
88
91
|
const additions = [...new Set(matched.map((a) => a.canonical))].join(" ");
|
|
89
92
|
return { expanded: `${query} ${additions}`, matched };
|
|
90
93
|
}
|
|
91
|
-
async function writeAliasNote(vaultPath, rel,
|
|
94
|
+
async function writeAliasNote(vaultPath, rel, raw) {
|
|
95
|
+
const entry = redactDeep(raw, "precise").value;
|
|
92
96
|
const fm = {
|
|
93
97
|
type: "alias",
|
|
94
98
|
canonical: entry.canonical,
|
|
@@ -173,4 +177,4 @@ export {
|
|
|
173
177
|
findAliasConflicts,
|
|
174
178
|
removeAlias
|
|
175
179
|
};
|
|
176
|
-
//# sourceMappingURL=chunk-
|
|
180
|
+
//# sourceMappingURL=chunk-FTRXH2WC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/lexicon.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, unlink } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"gray-matter\";\nimport { redactDeep } from \"./redact.js\";\nimport { deriveSessionDir } from \"./session.js\";\nimport { listDossiers } from \"./dossier.js\";\n\n/**\n * Lexicon corpus (v1.4 Phase 3) — the learned human→artifact vocabulary.\n *\n * The heart of the human→LLM bridge: maps the human's own phrasing (\"the flaky\n * thing\", \"the deploy dance\") to concrete artifacts (paths / code symbols).\n * Canonical storage is markdown notes of type `alias` under <vault>/lexicon/;\n * a derived JSON cache under .claude/.semantic-memory/ is what the hot-path hook\n * reads for deterministic query expansion.\n */\n\nexport type Confidence = \"low\" | \"medium\" | \"high\";\n\nexport interface AliasEntry {\n /** The concrete target: a note path or a code symbol/path. */\n canonical: string;\n /** The human's actual words that refer to it (verbatim). */\n phrases: string[];\n confidence: Confidence;\n evidence_count: number;\n last_used?: string;\n source: \"learned\" | \"authored\";\n /** The lexicon note path this entry came from. */\n path: string;\n}\n\nconst CACHE_FILE = \"lexicon-cache.json\";\nconst LEXICON_DIR = \"lexicon\";\n\nexport function normalizePhrase(s: string): string {\n return s.toLowerCase().trim().replace(/\\s+/g, \" \");\n}\n\nexport function slugify(s: string): string {\n return s.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 60) || \"alias\";\n}\n\nfunction confidenceFor(evidenceCount: number): Confidence {\n return evidenceCount >= 3 ? \"high\" : evidenceCount >= 2 ? \"medium\" : \"low\";\n}\n\n/** Read every lexicon note into entries and write the derived cache. */\nexport async function compileLexicon(vaultPath: string): Promise<AliasEntry[]> {\n const files = await glob(`${LEXICON_DIR}/**/*.md`, { cwd: vaultPath });\n const entries: AliasEntry[] = [];\n for (const rel of files) {\n try {\n const { data } = matter(await readFile(join(vaultPath, rel), \"utf-8\"));\n if (data.type !== \"alias\" || typeof data.canonical !== \"string\") continue;\n const phrases = Array.isArray(data.phrases) ? data.phrases.map(String).map((p) => p.trim()).filter(Boolean) : [];\n entries.push({\n canonical: data.canonical,\n phrases,\n confidence: ([\"low\", \"medium\", \"high\"] as const).includes(data.confidence) ? data.confidence : confidenceFor(1),\n evidence_count: typeof data.evidence_count === \"number\" ? data.evidence_count : phrases.length ? 1 : 0,\n last_used: typeof data.last_used === \"string\" ? data.last_used : undefined,\n source: data.source === \"authored\" ? \"authored\" : \"learned\",\n path: rel,\n });\n } catch {\n /* skip unreadable note */\n }\n }\n // Fold in dossier aliases (v1.5 Phase 8): a dossier's `aliases:` are the human's\n // phrases for that entity, so they become query-expansion bindings pointing at the\n // dossier note. Authored, not learned — the human named the entity deliberately.\n // A lexicon note for the same canonical wins (it carries real evidence_count).\n try {\n const dossiers = await listDossiers(vaultPath);\n for (const d of dossiers) {\n if (d.aliases.length === 0) continue;\n if (entries.some((e) => e.canonical === d.path)) continue;\n entries.push({\n canonical: d.path,\n phrases: d.aliases,\n confidence: \"high\",\n evidence_count: d.aliases.length,\n source: \"authored\",\n path: d.path,\n });\n }\n } catch {\n /* dossiers optional — lexicon compiles fine without them */\n }\n try {\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n await writeFile(join(dir, CACHE_FILE), JSON.stringify({ compiledAt: new Date().toISOString(), aliases: entries }, null, 2), \"utf-8\");\n } catch {\n /* cache write is best-effort */\n }\n return entries;\n}\n\n/** Fast read for the hook: use the cache if present, else compile. */\nexport async function loadLexiconCache(vaultPath: string): Promise<AliasEntry[]> {\n const cachePath = join(deriveSessionDir(vaultPath), CACHE_FILE);\n if (existsSync(cachePath)) {\n try {\n const data = JSON.parse(await readFile(cachePath, \"utf-8\"));\n if (Array.isArray(data?.aliases)) return data.aliases as AliasEntry[];\n } catch {\n /* fall through to recompile */\n }\n }\n return compileLexicon(vaultPath);\n}\n\nexport function lookupPhrase(aliases: AliasEntry[], phrase: string): AliasEntry[] {\n const n = normalizePhrase(phrase);\n return aliases.filter((a) => a.phrases.some((p) => normalizePhrase(p) === n));\n}\n\n/**\n * Expand a query by appending the canonical targets of any alias phrases that\n * appear in it. Deterministic, no LLM — this is the Tier-1 hook-time expansion.\n */\nexport function expandQuery(aliases: AliasEntry[], query: string): { expanded: string; matched: AliasEntry[] } {\n const nq = normalizePhrase(query);\n const matched = aliases.filter((a) => a.phrases.some((p) => p && nq.includes(normalizePhrase(p))));\n if (matched.length === 0) return { expanded: query, matched: [] };\n const additions = [...new Set(matched.map((a) => a.canonical))].join(\" \");\n return { expanded: `${query} ${additions}`, matched };\n}\n\nasync function writeAliasNote(vaultPath: string, rel: string, raw: Omit<AliasEntry, \"path\">): Promise<void> {\n // phrases are captured from user utterances\n const entry = redactDeep(raw, \"precise\").value;\n const fm = {\n type: \"alias\",\n canonical: entry.canonical,\n phrases: entry.phrases,\n confidence: entry.confidence,\n evidence_count: entry.evidence_count,\n last_used: entry.last_used,\n source: entry.source,\n };\n const body = `# Alias: ${entry.canonical}\\n\\nHuman phrases that refer to \\`${entry.canonical}\\`.\\n`;\n const abs = join(vaultPath, rel);\n await mkdir(dirname(abs), { recursive: true });\n await writeFile(abs, matter.stringify(body, fm), \"utf-8\");\n}\n\n/**\n * Upsert an alias. New canonical → new note (evidence 1). Existing canonical →\n * merge phrases, bump evidence_count, recompute confidence. Recompiles the cache.\n */\nexport async function addAlias(\n vaultPath: string,\n input: { canonical: string; phrases: string[]; source?: \"learned\" | \"authored\"; today?: string }\n): Promise<{ path: string; created: boolean; evidence_count: number; confidence: Confidence }> {\n const existing = await compileLexicon(vaultPath);\n const match = existing.find((a) => a.canonical === input.canonical);\n const today = input.today ?? new Date().toISOString().slice(0, 10);\n const cleanPhrases = input.phrases.map((p) => p.trim()).filter(Boolean);\n\n if (match) {\n const phrases = [...new Set([...match.phrases, ...cleanPhrases])];\n const evidence_count = match.evidence_count + 1;\n const confidence = confidenceFor(evidence_count);\n await writeAliasNote(vaultPath, match.path, { ...match, phrases, evidence_count, confidence, last_used: today });\n await compileLexicon(vaultPath);\n return { path: match.path, created: false, evidence_count, confidence };\n }\n\n const rel = join(LEXICON_DIR, `${slugify(input.canonical)}.md`);\n const evidence_count = 1;\n const confidence = confidenceFor(evidence_count);\n await writeAliasNote(vaultPath, rel, {\n canonical: input.canonical,\n phrases: cleanPhrases,\n confidence,\n evidence_count,\n last_used: today,\n source: input.source ?? \"learned\",\n });\n await compileLexicon(vaultPath);\n return { path: rel, created: true, evidence_count, confidence };\n}\n\nexport interface AliasConflict {\n phrase: string;\n canonicals: string[];\n}\n\n/**\n * Find conflicting bindings: the same (normalized) phrase mapping to more than one\n * canonical target. A wrong alias silently misroutes every future query with that\n * phrase, so conflicts are surfaced (never auto-resolved) for a human to fix.\n */\nexport function findAliasConflicts(aliases: AliasEntry[]): AliasConflict[] {\n const byPhrase = new Map<string, Set<string>>();\n for (const a of aliases) {\n for (const p of a.phrases) {\n const n = normalizePhrase(p);\n if (!n) continue;\n if (!byPhrase.has(n)) byPhrase.set(n, new Set());\n byPhrase.get(n)!.add(a.canonical);\n }\n }\n const conflicts: AliasConflict[] = [];\n for (const [phrase, canonicals] of byPhrase) {\n if (canonicals.size > 1) conflicts.push({ phrase, canonicals: [...canonicals].sort() });\n }\n return conflicts.sort((a, b) => a.phrase.localeCompare(b.phrase));\n}\n\n/** Remove an alias note by canonical. Returns whether one was removed. */\nexport async function removeAlias(vaultPath: string, canonical: string): Promise<boolean> {\n const existing = await compileLexicon(vaultPath);\n const match = existing.find((a) => a.canonical === canonical);\n if (!match) return false;\n try {\n await unlink(join(vaultPath, match.path));\n } catch {\n return false;\n }\n await compileLexicon(vaultPath);\n return true;\n}\n"],"mappings":"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","names":["evidence_count","confidence"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/schema.ts","../src/core/schema-default.ts"],"sourcesContent":["import { readFile, writeFile, access } from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport matter from \"gray-matter\";\nimport { DEFAULT_SCHEMA_YAML } from \"./schema-default.js\";\n\nexport interface VaultSchema {\n version: number;\n provenance_fields: string[];\n status_enum: string[];\n confidence_enum: string[];\n severity_enum: string[];\n types: Record<string, { description?: string; required: string[] }>;\n lint: {\n missing_provenance: { applies_to: string[]; severity: \"warn\" | \"error\" };\n stale: { max_age_days: number; severity: \"warn\" | \"error\" };\n schema_violations: { severity: \"warn\" | \"error\" };\n };\n}\n\nexport type Severity = \"warn\" | \"error\";\n\nexport interface LintFinding {\n path: string;\n rule: \"missing_provenance\" | \"stale\" | \"schema_violations\" | \"broken_links\" | \"code_symbols\" | \"decay_candidates\" | \"alias_conflicts\" | \"decoys\" | \"secrets\";\n severity: Severity;\n message: string;\n}\n\nexport async function loadSchema(vaultPath: string): Promise<VaultSchema> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n let raw: string;\n try {\n await access(schemaPath, constants.R_OK);\n raw = await readFile(schemaPath, \"utf-8\");\n } catch {\n raw = DEFAULT_SCHEMA_YAML;\n }\n const parsed = parseYaml(raw) as VaultSchema;\n return parsed;\n}\n\n/**\n * Bootstrap the schema file in a vault by writing the default if none exists.\n * Used by the `--install-schema` CLI flag. Returns the path that was (or would be) written.\n */\nexport async function installDefaultSchema(vaultPath: string, force = false): Promise<{ written: boolean; path: string }> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!force) {\n try {\n await access(schemaPath, constants.F_OK);\n return { written: false, path: schemaPath };\n } catch {\n // not present → write it\n }\n }\n await writeFile(schemaPath, DEFAULT_SCHEMA_YAML, \"utf-8\");\n return { written: true, path: schemaPath };\n}\n\n/**\n * Validate a single note's frontmatter + content against the schema.\n * Returns findings (possibly empty). Does NOT throw — lint never throws.\n */\nexport function validateNote(\n relativePath: string,\n rawFileContent: string,\n schema: VaultSchema,\n opts: { todayIso?: string } = {}\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const { data: fm } = matter(rawFileContent);\n\n const typeName = typeof fm.type === \"string\" ? fm.type : \"note\";\n const typeDef = schema.types[typeName];\n\n if (!typeDef) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `unknown type \"${typeName}\" — allowed: ${Object.keys(schema.types).join(\", \")}`,\n });\n return findings; // can't validate required fields if type is unknown\n }\n\n // Required field check\n for (const field of typeDef.required) {\n if (fm[field] === undefined || fm[field] === null || fm[field] === \"\") {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `missing required field \"${field}\" for type \"${typeName}\"`,\n });\n }\n }\n\n // Enum checks\n if (fm.status !== undefined && !schema.status_enum.includes(String(fm.status))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `status \"${fm.status}\" not in enum [${schema.status_enum.join(\", \")}]`,\n });\n }\n if (fm.confidence !== undefined && !schema.confidence_enum.includes(String(fm.confidence))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `confidence \"${fm.confidence}\" not in enum [${schema.confidence_enum.join(\", \")}]`,\n });\n }\n if (typeName === \"gotcha\" && fm.severity !== undefined && !schema.severity_enum.includes(String(fm.severity))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `severity \"${fm.severity}\" not in enum [${schema.severity_enum.join(\", \")}]`,\n });\n }\n\n // Missing provenance\n if (schema.lint.missing_provenance.applies_to.includes(typeName)) {\n const hasSources = Array.isArray(fm.sources) ? fm.sources.length > 0 : typeof fm.sources === \"string\" && fm.sources.length > 0;\n const hasDerived = Array.isArray(fm.derived_from) ? fm.derived_from.length > 0 : typeof fm.derived_from === \"string\" && fm.derived_from.length > 0;\n if (!hasSources && !hasDerived) {\n findings.push({\n path: relativePath,\n rule: \"missing_provenance\",\n severity: schema.lint.missing_provenance.severity,\n message: `no sources or derived_from on ${typeName}`,\n });\n }\n }\n\n // Stale\n if (schema.lint.stale.max_age_days > 0 && fm.last_verified) {\n const today = opts.todayIso ?? new Date().toISOString().slice(0, 10);\n const verified = String(fm.last_verified);\n const ageDays = dayDiff(today, verified);\n if (ageDays !== null && ageDays > schema.lint.stale.max_age_days) {\n findings.push({\n path: relativePath,\n rule: \"stale\",\n severity: schema.lint.stale.severity,\n message: `last_verified ${verified} is ${ageDays} days old (limit ${schema.lint.stale.max_age_days})`,\n });\n }\n }\n\n return findings;\n}\n\nfunction dayDiff(todayIso: string, thenIso: string): number | null {\n const a = Date.parse(todayIso);\n const b = Date.parse(thenIso);\n if (Number.isNaN(a) || Number.isNaN(b)) return null;\n return Math.round((a - b) / 86400000);\n}\n","// Default vault schema shipped with semantic-sidekick.\n// Intentionally minimal — the plan is to grow from pain, not preempt it.\n// Users can override with a vault.schema.yml at the vault root.\n\nexport const DEFAULT_SCHEMA_YAML = `# semantic-sidekick vault schema (minimal starter).\n# Override by placing a vault.schema.yml at the vault root.\n\nversion: 1\n\n# Provenance fields — apply to all types, enforced by lint.find_missing_provenance.\n# sources: URLs or filesystem paths the note was derived from\n# derived_from: wikilinks to other notes this one synthesizes\n# last_verified: ISO date this note was last confirmed true (YYYY-MM-DD).\n# Set at creation and by verify_note; NOT bumped on ordinary edits.\n# Drives confidence decay (see the decay: block below).\n# evergreen: true → the note is pinned against decay while last_verified is\n# within 365 days; after that it decays normally (re-affirm to reset).\n# status: one of: draft | active | stale | deprecated | archived\n# confidence: one of: high | medium | low\nprovenance_fields:\n - sources\n - derived_from\n - last_verified\n - status\n - confidence\n\n# Allowed status values (enforced by schema_violations lint).\nstatus_enum: [draft, active, stale, deprecated, archived]\nconfidence_enum: [high, medium, low]\n\n# Note types. Each type declares its required frontmatter.\n# Any field not listed as required is allowed but not enforced.\ntypes:\n note:\n description: Generic note — default type when \\`type\\` frontmatter is absent\n required: [title, status]\n\n decision:\n description: Architectural or process decision with dated rationale\n required: [title, status, decided_on, decision_maker]\n\n gotcha:\n description: Compressed lesson from a past incident/bug/surprise\n required: [title, status, last_verified, severity]\n\n source:\n description: External reference (paper, blog post, doc) used as provenance\n required: [title, status, source]\n\n alias:\n description: Learned human→artifact vocabulary entry (v1.4 lexicon). Maps the human's phrasing to a concrete path/symbol.\n required: [canonical]\n\n dossier:\n description: Entity-centric living note (v1.5). One per critical component — purpose, failure modes, knobs, incident log, current state. Accretes in place.\n required: [title, status, entity]\n\n profile:\n description: Speaker profile (v1.5). Models how THIS human communicates — severity calibration, chronic omissions, verbosity, shorthand. One human, one profile.\n required: [title, status]\n\n# Severity values for gotcha type.\nseverity_enum: [low, medium, high, critical]\n\n# Lint policy. Each rule has a default severity that pre-commit/CI consumes.\nlint:\n missing_provenance:\n # Triggers when a note type in this list has no sources AND no derived_from.\n applies_to: [note, decision, gotcha]\n severity: warn\n stale:\n # Triggers when last_verified is older than this many days (0 = disabled).\n max_age_days: 180\n severity: warn\n schema_violations:\n # Triggers on missing required fields, unknown type, or enum mismatch.\n severity: error\n\n# Confidence decay (v1.3). Down-weights search results by time since last_verified.\n# Composes multiplicatively with load_priority — it never removes notes, only ranks\n# them lower (floored). Set enabled: false for byte-identical pre-v1.3 ranking.\ndecay:\n enabled: true\n default_half_life_days: 365 # multiplier = 0.5 ^ (age_days / half_life)\n per_type:\n decision: 365\n note: 365\n gotcha: 180\n source: null # null = never decays\n proposal: 14\n floor: 0.1 # notes never drop below this multiplier\n hotness_boost:\n enabled: false # extend half-life for heavily-backlinked notes (opt-in)\n cap: 2.0\n\n# Path-class ranking (v1.4). Down-weights whole regions of the vault by path glob\n# (composes multiplicatively with decay + load_priority). First matching rule wins.\n# The default down-weights archived notes so retired docs stop dominating results.\npath_class:\n enabled: true\n rules:\n - glob: \"archive/**\"\n multiplier: 0.3\n\n# Usage-feedback ranking (v1.5). Boosts notes that get CITED after retrieval (per\n# the local selection log) so load-bearing knowledge floats up. Bounded by cap to\n# stop a feedback runaway; composes multiplicatively with decay + path_class +\n# load_priority. A no-op until the selection log has citations. The complementary\n# signal (retrieved-but-never-cited \"decoys\") is surfaced by lint_vault, NEVER\n# auto-down-ranked. Set enabled: false for byte-identical pre-v1.5 ranking.\nusage_boost:\n enabled: true\n cap: 1.5 # max multiplier a note can earn from citations\n per_citation: 0.1 # multiplier += per_citation × citation_count, then capped\n\n# Token-frugal tool surface (v1.4). When enabled, the server registers a reduced\n# tool set in modes that don't need the full surface (outage-silence → search/read\n# only). DEFAULT OFF — mid-session mode switches don't re-register, so opt in and\n# dogfood before relying on it.\ntools:\n conditional: false\n\n# Selection-logging telemetry (v1.3.1). Records what search returned and which\n# note the answer used, to a LOCAL append-only JSONL at\n# .claude/.semantic-memory/selection.jsonl (gitignored). Never leaves the machine.\n# Feeds the decay_candidates lint and (later) usage-feedback ranking. Set\n# enabled: false to disable all logging.\ntelemetry:\n enabled: true\n`;\n"],"mappings":";AAAA,SAAS,UAAU,WAAW,cAAc;AAC5C,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AACrB,SAAS,SAAS,iBAAiB;AACnC,OAAO,YAAY;;;ACAZ,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AD0BnC,eAAsB,WAAW,WAAyC;AACxE,QAAM,aAAa,KAAK,WAAW,kBAAkB;AACrD,MAAI;AACJ,MAAI;AACF,UAAM,OAAO,YAAY,UAAU,IAAI;AACvC,UAAM,MAAM,SAAS,YAAY,OAAO;AAAA,EAC1C,QAAQ;AACN,UAAM;AAAA,EACR;AACA,QAAM,SAAS,UAAU,GAAG;AAC5B,SAAO;AACT;AAMA,eAAsB,qBAAqB,WAAmB,QAAQ,OAAoD;AACxH,QAAM,aAAa,KAAK,WAAW,kBAAkB;AACrD,MAAI,CAAC,OAAO;AACV,QAAI;AACF,YAAM,OAAO,YAAY,UAAU,IAAI;AACvC,aAAO,EAAE,SAAS,OAAO,MAAM,WAAW;AAAA,IAC5C,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,UAAU,YAAY,qBAAqB,OAAO;AACxD,SAAO,EAAE,SAAS,MAAM,MAAM,WAAW;AAC3C;AAMO,SAAS,aACd,cACA,gBACA,QACA,OAA8B,CAAC,GAChB;AACf,QAAM,WAA0B,CAAC;AACjC,QAAM,EAAE,MAAM,GAAG,IAAI,OAAO,cAAc;AAE1C,QAAM,WAAW,OAAO,GAAG,SAAS,WAAW,GAAG,OAAO;AACzD,QAAM,UAAU,OAAO,MAAM,QAAQ;AAErC,MAAI,CAAC,SAAS;AACZ,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,MACxC,SAAS,iBAAiB,QAAQ,qBAAgB,OAAO,KAAK,OAAO,KAAK,EAAE,KAAK,IAAI,CAAC;AAAA,IACxF,CAAC;AACD,WAAO;AAAA,EACT;AAGA,aAAW,SAAS,QAAQ,UAAU;AACpC,QAAI,GAAG,KAAK,MAAM,UAAa,GAAG,KAAK,MAAM,QAAQ,GAAG,KAAK,MAAM,IAAI;AACrE,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,QACxC,SAAS,2BAA2B,KAAK,eAAe,QAAQ;AAAA,MAClE,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,GAAG,WAAW,UAAa,CAAC,OAAO,YAAY,SAAS,OAAO,GAAG,MAAM,CAAC,GAAG;AAC9E,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,MACxC,SAAS,WAAW,GAAG,MAAM,kBAAkB,OAAO,YAAY,KAAK,IAAI,CAAC;AAAA,IAC9E,CAAC;AAAA,EACH;AACA,MAAI,GAAG,eAAe,UAAa,CAAC,OAAO,gBAAgB,SAAS,OAAO,GAAG,UAAU,CAAC,GAAG;AAC1F,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,MACxC,SAAS,eAAe,GAAG,UAAU,kBAAkB,OAAO,gBAAgB,KAAK,IAAI,CAAC;AAAA,IAC1F,CAAC;AAAA,EACH;AACA,MAAI,aAAa,YAAY,GAAG,aAAa,UAAa,CAAC,OAAO,cAAc,SAAS,OAAO,GAAG,QAAQ,CAAC,GAAG;AAC7G,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,MACxC,SAAS,aAAa,GAAG,QAAQ,kBAAkB,OAAO,cAAc,KAAK,IAAI,CAAC;AAAA,IACpF,CAAC;AAAA,EACH;AAGA,MAAI,OAAO,KAAK,mBAAmB,WAAW,SAAS,QAAQ,GAAG;AAChE,UAAM,aAAa,MAAM,QAAQ,GAAG,OAAO,IAAI,GAAG,QAAQ,SAAS,IAAI,OAAO,GAAG,YAAY,YAAY,GAAG,QAAQ,SAAS;AAC7H,UAAM,aAAa,MAAM,QAAQ,GAAG,YAAY,IAAI,GAAG,aAAa,SAAS,IAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,aAAa,SAAS;AACjJ,QAAI,CAAC,cAAc,CAAC,YAAY;AAC9B,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAU,OAAO,KAAK,mBAAmB;AAAA,QACzC,SAAS,iCAAiC,QAAQ;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,OAAO,KAAK,MAAM,eAAe,KAAK,GAAG,eAAe;AAC1D,UAAM,QAAQ,KAAK,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACnE,UAAM,WAAW,OAAO,GAAG,aAAa;AACxC,UAAM,UAAU,QAAQ,OAAO,QAAQ;AACvC,QAAI,YAAY,QAAQ,UAAU,OAAO,KAAK,MAAM,cAAc;AAChE,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAU,OAAO,KAAK,MAAM;AAAA,QAC5B,SAAS,iBAAiB,QAAQ,OAAO,OAAO,oBAAoB,OAAO,KAAK,MAAM,YAAY;AAAA,MACpG,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,QAAQ,UAAkB,SAAgC;AACjE,QAAM,IAAI,KAAK,MAAM,QAAQ;AAC7B,QAAM,IAAI,KAAK,MAAM,OAAO;AAC5B,MAAI,OAAO,MAAM,CAAC,KAAK,OAAO,MAAM,CAAC,EAAG,QAAO;AAC/C,SAAO,KAAK,OAAO,IAAI,KAAK,KAAQ;AACtC;","names":[]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
redact
|
|
3
|
+
} from "./chunk-OEF5U5GV.js";
|
|
4
|
+
|
|
1
5
|
// src/core/session.ts
|
|
2
6
|
import { mkdir, readFile, writeFile, rm } from "fs/promises";
|
|
3
7
|
import { existsSync } from "fs";
|
|
@@ -34,7 +38,7 @@ var SessionManager = class {
|
|
|
34
38
|
const id = `ses_${now.replace(/[:.]/g, "-")}`;
|
|
35
39
|
const state = {
|
|
36
40
|
id,
|
|
37
|
-
task: task.trim(),
|
|
41
|
+
task: redact(task.trim(), "broad").text,
|
|
38
42
|
started_at: now,
|
|
39
43
|
last_activity_at: now,
|
|
40
44
|
verifications: [],
|
|
@@ -59,7 +63,8 @@ var SessionManager = class {
|
|
|
59
63
|
return { ok: false, error: "session_run requires a non-empty command." };
|
|
60
64
|
}
|
|
61
65
|
const start = Date.now();
|
|
62
|
-
const
|
|
66
|
+
const raw = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
67
|
+
const record = { ...raw, cmd: redact(raw.cmd, "broad").text, tail: redact(raw.tail, "broad").text };
|
|
63
68
|
record.duration_ms = Date.now() - start;
|
|
64
69
|
state.verifications.push(record);
|
|
65
70
|
state.last_activity_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -200,4 +205,4 @@ export {
|
|
|
200
205
|
deriveProjectRoot,
|
|
201
206
|
deriveSessionDir
|
|
202
207
|
};
|
|
203
|
-
//# sourceMappingURL=chunk-
|
|
208
|
+
//# sourceMappingURL=chunk-LGQPP5FQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/session.ts"],"sourcesContent":["import { mkdir, readFile, writeFile, rm } from \"node:fs/promises\";\nimport { redact } from \"./redact.js\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { spawn } from \"node:child_process\";\n\nexport interface VerificationRecord {\n cmd: string;\n exit: number | null;\n signal: string | null;\n duration_ms: number;\n tail: string;\n at: string;\n}\n\nexport interface SessionState {\n id: string;\n task: string;\n started_at: string;\n last_activity_at: string;\n verifications: VerificationRecord[];\n notes_touched: string[];\n closed_at?: string;\n closed_summary?: string;\n closed_verified_waived?: boolean;\n closed_waiver_reason?: string;\n}\n\nexport type SessionStatus =\n | { state: \"no_session\" }\n | (SessionState & { state: \"active\" | \"stale\" });\n\nexport interface SessionStartOptions {\n /** When true and a session for the same task is already open, return the existing id rather than failing. Defaults to true. */\n reuseSameTask?: boolean;\n}\n\nexport interface SessionFinishInput {\n summary: string;\n /**\n * When false, the caller is explicitly waiving verification (e.g. for doc-only edits).\n * Requires a non-empty `reason`. When true (default) and no verifications have been\n * recorded, the call is refused.\n */\n verified?: boolean;\n reason?: string;\n}\n\nconst STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\nconst TAIL_BYTES = 4096;\n\n/**\n * SessionManager owns a single session.json file at `<sessionDir>/session.json`.\n *\n * State is durable across MCP server restarts (the file persists). At most one session\n * is active per project at a time — concurrent sessions are explicitly out of scope\n * for v1.1; brain has the same constraint.\n *\n * Hard gates:\n * - session_finish refuses without verifications unless verified=false + reason\n * - session_run refuses without an open session\n * - session_start refuses if another session is open with a different task\n *\n * Stale detection: a session whose last_activity_at is >24h old is reported as\n * state:\"stale\" by status(). Stale sessions can still be finished or extended; the\n * label is informational so callers can decide whether to abandon (rm + start new) or\n * resume.\n */\nexport class SessionManager {\n constructor(private sessionDir: string) {}\n\n get sessionPath(): string {\n return join(this.sessionDir, \"session.json\");\n }\n\n async start(task: string, opts: SessionStartOptions = {}): Promise<{ ok: true; id: string; reused?: boolean } | { ok: false; error: string }> {\n if (!task || task.trim().length === 0) {\n return { ok: false, error: \"session_start requires a non-empty task.\" };\n }\n await mkdir(this.sessionDir, { recursive: true });\n const existing = await this.read();\n if (existing && !existing.closed_at) {\n const reuseSameTask = opts.reuseSameTask !== false;\n if (reuseSameTask && existing.task === task) {\n return { ok: true, id: existing.id, reused: true };\n }\n return {\n ok: false,\n error: `Session already open: id=${existing.id}, task=${JSON.stringify(existing.task)}. Close it with session_finish first.`,\n };\n }\n const now = new Date().toISOString();\n const id = `ses_${now.replace(/[:.]/g, \"-\")}`;\n const state: SessionState = {\n id,\n task: redact(task.trim(), \"broad\").text,\n started_at: now,\n last_activity_at: now,\n verifications: [],\n notes_touched: [],\n };\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n return { ok: true, id };\n }\n\n async status(): Promise<SessionStatus> {\n const state = await this.read();\n if (!state || state.closed_at) return { state: \"no_session\" };\n const age = Date.now() - new Date(state.last_activity_at).getTime();\n const label: \"active\" | \"stale\" = age > STALE_THRESHOLD_MS ? \"stale\" : \"active\";\n return { ...state, state: label };\n }\n\n async run(cmd: string, timeoutMs?: number): Promise<{ ok: true; record: VerificationRecord } | { ok: false; error: string }> {\n const state = await this.read();\n if (!state || state.closed_at) {\n return { ok: false, error: \"No active session. Run session_start first.\" };\n }\n if (!cmd || cmd.trim().length === 0) {\n return { ok: false, error: \"session_run requires a non-empty command.\" };\n }\n const start = Date.now();\n const raw = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);\n // session.json keeps the command and its output tail: either can carry a token\n const record = { ...raw, cmd: redact(raw.cmd, \"broad\").text, tail: redact(raw.tail, \"broad\").text };\n record.duration_ms = Date.now() - start;\n state.verifications.push(record);\n state.last_activity_at = new Date().toISOString();\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n return { ok: true, record };\n }\n\n async finish(input: SessionFinishInput): Promise<{ ok: true; closed: SessionState } | { ok: false; error: string }> {\n const state = await this.read();\n if (!state || state.closed_at) {\n return { ok: false, error: \"No active session to finish.\" };\n }\n const explicitWaiver = input.verified === false;\n if (!explicitWaiver && state.verifications.length === 0) {\n return {\n ok: false,\n error:\n \"session_finish refused: no verification commands recorded. Run session_run with a real verification command (tests, lint, etc.) first, OR pass verified=false with a reason to waive verification (e.g. for doc-only edits).\",\n };\n }\n if (explicitWaiver && (!input.reason || input.reason.trim().length === 0)) {\n return {\n ok: false,\n error: \"session_finish refused: verified=false requires a non-empty `reason` explaining why verification is not applicable to this task.\",\n };\n }\n if (!input.summary || input.summary.trim().length === 0) {\n return { ok: false, error: \"session_finish requires a non-empty `summary`.\" };\n }\n const closed: SessionState = {\n ...state,\n closed_at: new Date().toISOString(),\n closed_summary: input.summary.trim(),\n ...(explicitWaiver && {\n closed_verified_waived: true,\n closed_waiver_reason: input.reason!.trim(),\n }),\n };\n // Sessions are ephemeral runtime state — finished sessions are removed from disk.\n // Durable artifacts (synthesize_note proposals, log entries) capture what mattered.\n await rm(this.sessionPath, { force: true });\n return { ok: true, closed };\n }\n\n /**\n * Append paths to notes_touched on the active session, deduplicating. No-op when no\n * session is active. Used by patch tools to record what the session edited.\n */\n async recordNotesTouched(paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const state = await this.read();\n if (!state || state.closed_at) return;\n const set = new Set(state.notes_touched);\n for (const p of paths) set.add(p);\n state.notes_touched = Array.from(set);\n state.last_activity_at = new Date().toISOString();\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n }\n\n /**\n * Read the active session WITHOUT computing the stale label. Returns null when no\n * session is active or when the file is unparseable.\n */\n async readActive(): Promise<SessionState | null> {\n const state = await this.read();\n if (!state || state.closed_at) return null;\n return state;\n }\n\n private async read(): Promise<SessionState | null> {\n if (!existsSync(this.sessionPath)) return null;\n try {\n const raw = await readFile(this.sessionPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (typeof parsed?.id !== \"string\") return null;\n return parsed as SessionState;\n } catch {\n return null;\n }\n }\n\n private spawnAndCapture(cmd: string, timeoutMs: number): Promise<VerificationRecord> {\n return new Promise((resolve) => {\n const proc = spawn(cmd, { shell: true });\n let buf = \"\";\n let killedByTimeout = false;\n const timer = setTimeout(() => {\n killedByTimeout = true;\n proc.kill(\"SIGTERM\");\n setTimeout(() => {\n try { proc.kill(\"SIGKILL\"); } catch { /* already gone */ }\n }, 1000).unref();\n }, timeoutMs);\n\n const onData = (chunk: Buffer) => {\n buf += chunk.toString(\"utf-8\");\n if (buf.length > TAIL_BYTES * 4) {\n buf = buf.slice(buf.length - TAIL_BYTES * 2);\n }\n };\n proc.stdout?.on(\"data\", onData);\n proc.stderr?.on(\"data\", onData);\n\n proc.on(\"close\", (code, signal) => {\n clearTimeout(timer);\n const tail = buf.slice(-TAIL_BYTES);\n resolve({\n cmd,\n exit: killedByTimeout ? null : code,\n signal: killedByTimeout ? \"SIGTERM (timeout)\" : (signal ?? null),\n duration_ms: 0,\n tail,\n at: new Date().toISOString(),\n });\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n const tail = buf + `\\n[spawn error: ${err.message}]`;\n resolve({\n cmd,\n exit: null,\n signal: null,\n duration_ms: 0,\n tail: tail.slice(-TAIL_BYTES),\n at: new Date().toISOString(),\n });\n });\n });\n }\n}\n\n/**\n * Heuristic: derive the project root from the vault path. If the vault path ends in\n * `.claude/.vault`, the project root is two levels up. Otherwise the vault path is\n * treated as the project root.\n */\nexport function deriveProjectRoot(notesPath: string): string {\n const norm = notesPath.replace(/[\\\\/]+$/, \"\");\n if (norm.endsWith(\"/.claude/.vault\") || norm.endsWith(\"\\\\.claude\\\\.vault\")) {\n return norm.slice(0, -\".claude/.vault\".length).replace(/[\\\\/]+$/, \"\");\n }\n return norm;\n}\n\nexport function deriveSessionDir(notesPath: string): string {\n return join(deriveProjectRoot(notesPath), \".claude\", \".semantic-memory\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deriveSessionDir
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LGQPP5FQ.js";
|
|
4
|
+
import {
|
|
5
|
+
redact
|
|
6
|
+
} from "./chunk-OEF5U5GV.js";
|
|
4
7
|
|
|
5
8
|
// src/core/telemetry.ts
|
|
6
9
|
import { appendFile, readFile, mkdir } from "fs/promises";
|
|
@@ -28,7 +31,8 @@ async function appendEvent(vaultPath, event, nowIso) {
|
|
|
28
31
|
if (!telemetryEnabled(vaultPath)) return;
|
|
29
32
|
const dir = deriveSessionDir(vaultPath);
|
|
30
33
|
await mkdir(dir, { recursive: true });
|
|
31
|
-
const
|
|
34
|
+
const safe = event.kind === "search" ? { ...event, query: redact(event.query, "broad").text } : event;
|
|
35
|
+
const line = JSON.stringify({ ts: nowIso ?? (/* @__PURE__ */ new Date()).toISOString(), ...safe });
|
|
32
36
|
await appendFile(selectionLogPath(vaultPath), line + "\n", "utf-8");
|
|
33
37
|
} catch {
|
|
34
38
|
}
|
|
@@ -88,4 +92,4 @@ export {
|
|
|
88
92
|
readSelectionLog,
|
|
89
93
|
computeSelectionStats
|
|
90
94
|
};
|
|
91
|
-
//# sourceMappingURL=chunk-
|
|
95
|
+
//# sourceMappingURL=chunk-NBY4TRP7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/telemetry.ts"],"sourcesContent":["import { appendFile, readFile, mkdir } from \"node:fs/promises\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { redact } from \"./redact.js\";\nimport { parse as parseYaml } from \"yaml\";\nimport { deriveSessionDir } from \"./session.js\";\n\n/**\n * Selection-logging telemetry (v1.3.1).\n *\n * Records what retrieval actually returned and which note the answer used, so a\n * later release (v1.4 usage-feedback ranking) can learn which knowledge is\n * load-bearing. Strictly local, append-only JSONL, fire-and-forget, opt-out.\n *\n * NO NETWORK: this module performs local filesystem I/O only, and never opens a\n * socket or issues a remote request — a unit test greps this file to keep it that way.\n */\n\nconst LOG_FILE = \"selection.jsonl\";\nconst CORRELATION_WINDOW_MS = 60_000;\n\nexport interface SearchEvent {\n kind: \"search\";\n tool: string;\n query: string;\n results: { path: string; score: number; decay?: number }[];\n}\n\nexport interface SelectionEvent {\n kind: \"selection\";\n note_path: string;\n via: string;\n /** True when this selection followed a recent search that returned this path. */\n correlated?: boolean;\n}\n\nexport type TelemetryEvent = (SearchEvent | SelectionEvent) & { ts?: string };\n\n/** Absolute path to the selection log for a given vault. */\nexport function selectionLogPath(vaultPath: string): string {\n return join(deriveSessionDir(vaultPath), LOG_FILE);\n}\n\n/**\n * Whether telemetry is enabled. Reads `telemetry.enabled` from vault.schema.yml;\n * defaults true (opt-out, not opt-in). Fails open to enabled only if the schema\n * is unreadable — a missing `telemetry` block means \"use the default (true)\".\n */\nexport function telemetryEnabled(vaultPath: string): boolean {\n try {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!existsSync(schemaPath)) return true;\n const parsed = parseYaml(readFileSync(schemaPath, \"utf-8\")) as { telemetry?: { enabled?: boolean } } | null;\n const e = parsed?.telemetry?.enabled;\n return typeof e === \"boolean\" ? e : true;\n } catch {\n return true;\n }\n}\n\n/**\n * Append one event. Fire-and-forget: respects the opt-out and NEVER throws — a\n * telemetry failure must not affect the tool call that triggered it.\n */\nexport async function appendEvent(vaultPath: string, event: TelemetryEvent, nowIso?: string): Promise<void> {\n try {\n if (!telemetryEnabled(vaultPath)) return;\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n // only the free-text query: note_path feeds usage-boost citation counts and must stay intact\n const safe = event.kind === \"search\" ? { ...event, query: redact(event.query, \"broad\").text } : event;\n const line = JSON.stringify({ ts: nowIso ?? new Date().toISOString(), ...safe });\n await appendFile(selectionLogPath(vaultPath), line + \"\\n\", \"utf-8\");\n } catch {\n // fire-and-forget\n }\n}\n\n// --- In-process correlation ring ---\n// Remembers the most recent search's top-k paths per vault so a subsequent\n// read/synthesize can be attributed to it (best-effort, time-bounded).\n\nconst recentSearch = new Map<string, { paths: Set<string>; atMs: number }>();\n\nexport function recordSearchForCorrelation(vaultPath: string, paths: string[], nowMs?: number): void {\n recentSearch.set(vaultPath, { paths: new Set(paths), atMs: nowMs ?? Date.now() });\n}\n\nexport function wasRecentlySearched(vaultPath: string, notePath: string, nowMs?: number): boolean {\n const r = recentSearch.get(vaultPath);\n if (!r) return false;\n if ((nowMs ?? Date.now()) - r.atMs > CORRELATION_WINDOW_MS) return false;\n return r.paths.has(notePath);\n}\n\n/** Read the selection log (oldest→newest). Returns [] if absent/unreadable. */\nexport async function readSelectionLog(vaultPath: string, opts: { limit?: number } = {}): Promise<TelemetryEvent[]> {\n try {\n const p = selectionLogPath(vaultPath);\n if (!existsSync(p)) return [];\n const raw = await readFile(p, \"utf-8\");\n const events = raw\n .split(\"\\n\")\n .filter((l) => l.trim())\n .map((l) => {\n try {\n return JSON.parse(l) as TelemetryEvent;\n } catch {\n return null;\n }\n })\n .filter((e): e is TelemetryEvent => e !== null);\n return opts.limit ? events.slice(-opts.limit) : events;\n } catch {\n return [];\n }\n}\n\n/** For CLI/analysis: cited-vs-retrieved rollup over the log. */\nexport interface SelectionStats {\n searches: number;\n selections: number;\n citedPaths: Record<string, number>;\n retrievedNeverCited: string[];\n}\n\nexport async function computeSelectionStats(vaultPath: string): Promise<SelectionStats> {\n const events = await readSelectionLog(vaultPath);\n const citedPaths: Record<string, number> = {};\n const retrieved = new Set<string>();\n let searches = 0;\n let selections = 0;\n for (const e of events) {\n if (e.kind === \"search\") {\n searches++;\n for (const r of e.results) retrieved.add(r.path);\n } else if (e.kind === \"selection\") {\n selections++;\n citedPaths[e.note_path] = (citedPaths[e.note_path] ?? 0) + 1;\n }\n }\n const retrievedNeverCited = [...retrieved].filter((p) => !(p in citedPaths)).sort();\n return { searches, selections, citedPaths, retrievedNeverCited };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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
|