@prom.codes/memory-mcp 0.11.4 → 0.14.0

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 (2) hide show
  1. package/dist/bin.js +1152 -136
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -428,8 +428,8 @@ function createIdleWatchdog(options) {
428
428
 
429
429
  // dist/composition.js
430
430
  import { createHash } from "node:crypto";
431
- import { homedir as homedir4 } from "node:os";
432
- import { basename, join as join4, resolve as resolve2 } from "node:path";
431
+ import { homedir as homedir5 } from "node:os";
432
+ import { basename, join as join6, resolve as resolve2 } from "node:path";
433
433
 
434
434
  // ../embeddings-openai-compat/dist/index.js
435
435
  var DEFAULT_BATCH = 96;
@@ -1548,6 +1548,26 @@ function requireApiKey(env) {
1548
1548
 
1549
1549
  // dist/extraction.js
1550
1550
  var SYSTEM_PROMPT = 'You extract durable, atomic facts from a coding agent\'s session notes for long-term project memory. Output ONLY a JSON array of objects {"key":..., "value":...}. Each fact must be ONE self-contained statement that will be useful in a FUTURE session: a decision, a convention, a preference, a stable configuration, or a learned fact about the project. `key` is a short kebab-case slug; `value` is the full fact in one sentence. DROP transient step-by-step narration, anything true only for this one session, and anything obvious. Never invent facts not supported by the notes. If there is nothing durable, output []. Output at most 12 facts.';
1551
+ var CURATION_SYSTEM_PROMPT = 'You curate a coding agent\'s recorded session into long-term project memory. Output ONLY a JSON object: {"summary": string, "facts": [{"key","value","confidence"}], "procedures": [{"key","value"}]}. `summary` is 3-6 sentences: the goal, the outcome, and the key steps taken. `facts` are DURABLE things worth recalling in a future session \u2014 decisions, conventions, preferences, stable config, or learned facts about the project. `procedures` are reusable, proven how-tos (a sequence of steps that worked). For each candidate `key` is a short kebab-case slug and `value` is self-contained. DROP transient narration and anything true only for this one run. Never invent anything not supported by the log. If nothing is durable, return empty arrays (still write the summary). At most 10 facts and 6 procedures.';
1552
+ function parseCuration(raw, maxSummaryChars = 2e3) {
1553
+ const match = raw.match(/\{[\s\S]*\}/);
1554
+ const empty = { summary: "", facts: [], procedures: [] };
1555
+ if (!match)
1556
+ return empty;
1557
+ let parsed;
1558
+ try {
1559
+ parsed = JSON.parse(match[0]);
1560
+ } catch {
1561
+ return empty;
1562
+ }
1563
+ if (typeof parsed !== "object" || parsed === null)
1564
+ return empty;
1565
+ const obj = parsed;
1566
+ const summary = typeof obj.summary === "string" ? obj.summary.trim().slice(0, maxSummaryChars) : "";
1567
+ const facts = Array.isArray(obj.facts) ? parseExtraction(JSON.stringify(obj.facts), 10) : [];
1568
+ const procedures = Array.isArray(obj.procedures) ? parseExtraction(JSON.stringify(obj.procedures), 6) : [];
1569
+ return { summary, facts, procedures };
1570
+ }
1551
1571
  function parseExtraction(raw, maxFacts = 12, maxValueChars = 2e3) {
1552
1572
  const match = raw.match(/\[[\s\S]*\]/);
1553
1573
  if (!match)
@@ -1607,47 +1627,60 @@ var OpenAICompatExtractor = class {
1607
1627
  const trimmed = text.trim();
1608
1628
  if (trimmed === "")
1609
1629
  return [];
1630
+ const content = await this.#chat(SYSTEM_PROMPT, `Session notes:
1631
+
1632
+ ${trimmed}`, opts?.signal);
1633
+ return content === null ? [] : parseExtraction(content);
1634
+ }
1635
+ async curate(sessionText, opts) {
1636
+ const trimmed = sessionText.trim();
1637
+ const empty = { summary: "", facts: [], procedures: [] };
1638
+ if (trimmed === "")
1639
+ return empty;
1640
+ const content = await this.#chat(CURATION_SYSTEM_PROMPT, `Session log:
1641
+
1642
+ ${trimmed}`, opts?.signal);
1643
+ return content === null ? empty : parseCuration(content);
1644
+ }
1645
+ /**
1646
+ * One chat-completion round with retry on 429/5xx. Returns the message content,
1647
+ * or `null` on a permanent client error / exhausted retries (callers degrade
1648
+ * gracefully — never throw). Shared by {@link extract} and {@link curate}.
1649
+ */
1650
+ async #chat(system, user, signal) {
1610
1651
  const body = JSON.stringify({
1611
1652
  model: this.model,
1612
1653
  temperature: this.#temperature,
1613
1654
  messages: [
1614
- { role: "system", content: SYSTEM_PROMPT },
1615
- { role: "user", content: `Session notes:
1616
-
1617
- ${trimmed}` }
1655
+ { role: "system", content: system },
1656
+ { role: "user", content: user }
1618
1657
  ]
1619
1658
  });
1620
1659
  const headers = { "content-type": "application/json" };
1621
1660
  if (this.#apiKey !== void 0 && this.#apiKey !== "") {
1622
1661
  headers.authorization = `Bearer ${this.#apiKey}`;
1623
1662
  }
1624
- let lastErr;
1625
1663
  for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
1626
1664
  try {
1627
1665
  const res = await this.#fetch(this.#url, {
1628
1666
  method: "POST",
1629
1667
  headers,
1630
1668
  body,
1631
- ...opts?.signal ? { signal: opts.signal } : {}
1669
+ ...signal ? { signal } : {}
1632
1670
  });
1633
1671
  if (res.status === 429 || res.status >= 500) {
1634
- lastErr = new Error(`extractor HTTP ${res.status}`);
1635
1672
  } else if (!res.ok) {
1636
- return [];
1673
+ return null;
1637
1674
  } else {
1638
1675
  const json = await res.json();
1639
- const content = json.choices?.[0]?.message?.content ?? "";
1640
- return parseExtraction(content);
1676
+ return json.choices?.[0]?.message?.content ?? "";
1641
1677
  }
1642
- } catch (err) {
1643
- lastErr = err;
1678
+ } catch {
1644
1679
  }
1645
- if (attempt < this.#maxRetries) {
1680
+ if (attempt < this.#maxRetries)
1646
1681
  await delay(this.#retryBaseMs * 2 ** attempt);
1647
- }
1648
1682
  }
1649
- void lastErr;
1650
- return [];
1683
+ return null;
1651
1684
  }
1652
1685
  };
1653
1686
  function delay(ms) {
@@ -1729,10 +1762,347 @@ var OpenAICompatRewriter = class {
1729
1762
 
1730
1763
  // dist/sqlite.js
1731
1764
  import { randomUUID } from "node:crypto";
1732
- import { mkdirSync as mkdirSync2 } from "node:fs";
1733
- import { dirname as dirname2 } from "node:path";
1765
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
1766
+ import { dirname as dirname3, join as join5 } from "node:path";
1734
1767
  import Database from "better-sqlite3";
1735
1768
 
1769
+ // dist/security.js
1770
+ var SECRET_PATTERNS = [
1771
+ { name: "openai-key", regex: /\bsk-proj-[A-Za-z0-9_-]{20,}/ },
1772
+ { name: "anthropic-key", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
1773
+ { name: "supabase-token", regex: /\bsbp?_[A-Za-z0-9]{20,}/ },
1774
+ { name: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}/ },
1775
+ { name: "gitlab-token", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
1776
+ { name: "dockerhub-token", regex: /\bdckr_(?:pat|oat)_[A-Za-z0-9_-]{10,}/ },
1777
+ { name: "resend-key", regex: /\bre_[A-Za-z0-9]{8,}_[A-Za-z0-9]{10,}/ },
1778
+ { name: "runpod-key", regex: /\brpa_[A-Za-z0-9]{30,}/ },
1779
+ { name: "sentry-token", regex: /\bsntrys_[A-Za-z0-9+/=_-]{20,}/ },
1780
+ { name: "vercel-token", regex: /\bvc[kp]_[A-Za-z0-9]{20,}/ },
1781
+ { name: "huggingface-token", regex: /\bhf_[A-Za-z0-9]{30,}/ },
1782
+ { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{30,}/ },
1783
+ { name: "voyage-key", regex: /\bpa-[A-Za-z0-9_-]{30,}/ },
1784
+ { name: "google-api-key", regex: /\bAIza[A-Za-z0-9_-]{30,}/ },
1785
+ { name: "sovrgpt-key", regex: /\bsov_[a-f0-9]{40,}/ },
1786
+ { name: "prometheus-key", regex: /\bprom_(?:live|test)_[A-Za-z0-9]{10,}/ },
1787
+ { name: "aws-access-key", regex: /\bAKIA[A-Z0-9]{16}\b/ },
1788
+ { name: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/ },
1789
+ { name: "private-key-block", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
1790
+ { name: "authorization-header", regex: /\bAuthorization:\s*(?:Bearer|Basic)\s+\S{8,}/i },
1791
+ {
1792
+ name: "connection-string-credentials",
1793
+ regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s/@:]+:[^\s/@]+@/i
1794
+ }
1795
+ ];
1796
+ function findSecretPatterns(text) {
1797
+ const hits = [];
1798
+ for (const p of SECRET_PATTERNS) {
1799
+ if (p.regex.test(text))
1800
+ hits.push(p.name);
1801
+ }
1802
+ return hits;
1803
+ }
1804
+ var SECRET_VALUE_ERROR = "memory value matches the secret deny-list and was rejected";
1805
+ function assertNoSecrets(text) {
1806
+ const hits = findSecretPatterns(text);
1807
+ if (hits.length > 0) {
1808
+ throw new Error(`${SECRET_VALUE_ERROR} (pattern: ${hits.join(", ")}).`);
1809
+ }
1810
+ }
1811
+
1812
+ // dist/recorder.js
1813
+ import { copyFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1814
+ import { homedir as homedir4 } from "node:os";
1815
+ import { dirname as dirname2, join as join4 } from "node:path";
1816
+ var SPOOL_VERSION = 1;
1817
+ var SECRET_PATTERN_SOURCES = [
1818
+ "sk-proj-[A-Za-z0-9_-]{20,}",
1819
+ "sk-ant-[A-Za-z0-9_-]{20,}",
1820
+ "\\bsbp?_[A-Za-z0-9]{20,}",
1821
+ "\\bgh[pousr]_[A-Za-z0-9]{20,}",
1822
+ "\\bglpat-[A-Za-z0-9_-]{20,}",
1823
+ "\\bdckr_(?:pat|oat)_[A-Za-z0-9_-]{10,}",
1824
+ "\\bre_[A-Za-z0-9]{8,}_[A-Za-z0-9]{10,}",
1825
+ "\\brpa_[A-Za-z0-9]{30,}",
1826
+ "\\bsntrys_[A-Za-z0-9+/=_-]{20,}",
1827
+ "\\bvc[kp]_[A-Za-z0-9]{20,}",
1828
+ "\\bhf_[A-Za-z0-9]{30,}",
1829
+ "\\bnpm_[A-Za-z0-9]{30,}",
1830
+ "\\bpa-[A-Za-z0-9_-]{30,}",
1831
+ "\\bAIza[A-Za-z0-9_-]{30,}",
1832
+ "\\bsov_[a-f0-9]{40,}",
1833
+ "\\bprom_(?:live|test)_[A-Za-z0-9]{10,}",
1834
+ "\\bAKIA[A-Z0-9]{16}\\b",
1835
+ "\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}",
1836
+ "-----BEGIN [A-Z ]*PRIVATE KEY-----",
1837
+ "\\b(?:postgres(?:ql)?|mysql|mongodb(?:\\+srv)?|redis|amqp)://[^\\s/@:]+:[^\\s/@]+@"
1838
+ ];
1839
+ var REDACT_RE = new RegExp(`(${SECRET_PATTERN_SOURCES.join(")|(")})`, "gi");
1840
+ var EVENT_CAP_BYTES = 4 * 1024;
1841
+ var SPOOL_CAP_BYTES = 4 * 1024 * 1024;
1842
+ function recorderRoot(env = process.env) {
1843
+ const base = env.PROMETHEUS_DIR && env.PROMETHEUS_DIR !== "" ? env.PROMETHEUS_DIR : join4(homedir4(), ".prometheus");
1844
+ return join4(base, "recorder");
1845
+ }
1846
+ function sanitizeSegment(s) {
1847
+ return s.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128) || "_";
1848
+ }
1849
+ function parseSpool(raw) {
1850
+ const lines = raw.split("\n");
1851
+ const events = [];
1852
+ let droppedPartialTail = false;
1853
+ let ended = false;
1854
+ for (let i = 0; i < lines.length; i++) {
1855
+ const line = lines[i];
1856
+ if (line.trim() === "")
1857
+ continue;
1858
+ try {
1859
+ const obj = JSON.parse(line);
1860
+ if (obj && typeof obj.event === "string" && typeof obj.sessionId === "string") {
1861
+ events.push({ ...obj, seq: i });
1862
+ if (obj.event === "session_end")
1863
+ ended = true;
1864
+ }
1865
+ } catch {
1866
+ if (i === lines.length - 1)
1867
+ droppedPartialTail = true;
1868
+ }
1869
+ }
1870
+ return { events, droppedPartialTail, ended };
1871
+ }
1872
+ var RECORDER_HOOK_FILENAME = "prometheus-recorder-hook.mjs";
1873
+ var RECORDER_HOOK_SCRIPT = String.raw`#!/usr/bin/env node
1874
+ // Prometheus Session Recorder hook (Claude Code). Appends ONE diet+redacted
1875
+ // JSONL event per invocation to ~/.prometheus/recorder/<projectId>/<sessionId>.jsonl.
1876
+ // Pure node stdlib, no deps. EXITS 0 ON EVERY PATH — a recorder failure must
1877
+ // never disturb a coding session. Managed by prom.codes (memory-mcp recorder_setup).
1878
+ import { appendFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
1879
+ import { homedir } from "node:os";
1880
+ import { join } from "node:path";
1881
+ import { createHash } from "node:crypto";
1882
+
1883
+ const SPOOL_CAP = ${SPOOL_CAP_BYTES};
1884
+ const EVENT_CAP = ${EVENT_CAP_BYTES};
1885
+ const SECRET_SOURCES = ${JSON.stringify(SECRET_PATTERN_SOURCES)};
1886
+ const REDACT = new RegExp("(" + SECRET_SOURCES.join(")|(") + ")", "gi");
1887
+
1888
+ function redact(s) { return String(s).replace(REDACT, "[redacted]"); }
1889
+ function clip(s, n) {
1890
+ const str = typeof s === "string" ? s : (s === undefined ? "" : JSON.stringify(s));
1891
+ return str.length > n ? str.slice(0, n) + "…" : str;
1892
+ }
1893
+ function sanitize(s) { return String(s).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128) || "_"; }
1894
+ function sensitivePath(p) {
1895
+ const s = String(p).replace(/\\/g, "/").toLowerCase();
1896
+ const base = s.slice(s.lastIndexOf("/") + 1);
1897
+ if (/^\.env(\..+)?$/.test(base)) return true;
1898
+ if (base === "id_rsa" || base === "id_dsa" || base === "id_ecdsa" || base === "id_ed25519") return true;
1899
+ if (/\.(pem|key|pfx|p12|keystore)$/.test(base)) return true;
1900
+ if (/(^|\/)(secrets?|credentials?)(\/|$)/.test(s)) return true;
1901
+ return false;
1902
+ }
1903
+ function projectIdOf(root) { return createHash("sha256").update(String(root)).digest("hex").slice(0, 16); }
1904
+
1905
+ function dietTool(toolName, inp, result) {
1906
+ const tool = toolName; inp = inp || {};
1907
+ const fp = typeof inp.file_path === "string" ? inp.file_path : undefined;
1908
+ if (fp !== undefined && sensitivePath(fp)) return { tool: tool, note: "[skipped: sensitive path]" };
1909
+ if (tool === "Edit" || tool === "Write" || tool === "NotebookEdit") {
1910
+ return { tool: tool, file_path: fp, oldPreview: clip(inp.old_string || "", 100), newPreview: clip(inp.new_string || inp.content || "", 100) };
1911
+ }
1912
+ if (tool === "Bash") {
1913
+ const r = result || {};
1914
+ return { tool: tool, command: clip(inp.command, 300), stdout: clip(r.stdout, 500), stderr: clip(r.stderr, 200), exitCode: typeof r.exitCode === "number" ? r.exitCode : (r.exit_code != null ? r.exit_code : null) };
1915
+ }
1916
+ if (tool === "Read" || tool === "Glob" || tool === "Grep" || tool === "LS") {
1917
+ return { tool: tool, target: clip(inp.file_path || inp.path || inp.pattern || "", 200) };
1918
+ }
1919
+ return { tool: tool, resultPreview: clip(result, 500) };
1920
+ }
1921
+
1922
+ function finalize(payload) {
1923
+ const s = redact(JSON.stringify(payload));
1924
+ return s.length > EVENT_CAP ? s.slice(0, EVENT_CAP) + "…" : s;
1925
+ }
1926
+
1927
+ // Read the last assistant text from a Claude Code transcript JSONL (best-effort).
1928
+ function lastAssistantText(transcriptPath) {
1929
+ try {
1930
+ if (!transcriptPath) return "";
1931
+ const raw = readFileSync(transcriptPath, "utf8");
1932
+ const lines = raw.split("\n");
1933
+ for (let i = lines.length - 1; i >= 0; i--) {
1934
+ const ln = lines[i].trim(); if (ln === "") continue;
1935
+ let obj; try { obj = JSON.parse(ln); } catch { continue; }
1936
+ const msg = obj && obj.message ? obj.message : obj;
1937
+ const role = obj && obj.type ? obj.type : (msg && msg.role);
1938
+ if (role === "assistant" && msg) {
1939
+ const c = msg.content;
1940
+ if (typeof c === "string") return c;
1941
+ if (Array.isArray(c)) {
1942
+ const txt = c.filter(function (b) { return b && b.type === "text"; }).map(function (b) { return b.text; }).join("\n");
1943
+ if (txt) return txt;
1944
+ }
1945
+ }
1946
+ }
1947
+ } catch { /* best-effort */ }
1948
+ return "";
1949
+ }
1950
+
1951
+ function run(input) {
1952
+ let p = {};
1953
+ try { p = JSON.parse(String(input || "").replace(/^/, "").trim() || "{}"); } catch { return; }
1954
+ const eventName = typeof p.hook_event_name === "string" ? p.hook_event_name : "";
1955
+ const sessionId = typeof p.session_id === "string" && p.session_id !== "" ? p.session_id : "unknown";
1956
+ const root = (process.env.CLAUDE_PROJECT_DIR && process.env.CLAUDE_PROJECT_DIR !== "") ? process.env.CLAUDE_PROJECT_DIR : (p.cwd || process.cwd());
1957
+ const projectId = projectIdOf(root);
1958
+
1959
+ let event = null; let payload = {};
1960
+ if (eventName === "SessionStart") { event = "session_start"; payload = { cwd: root, model: p.model || null, source: p.source || null }; }
1961
+ else if (eventName === "UserPromptSubmit") { event = "user_message"; payload = { text: clip(p.prompt || "", 4000) }; }
1962
+ else if (eventName === "PostToolUse") { event = "tool_use"; payload = dietTool(p.tool_name || "", p.tool_input, p.tool_response); }
1963
+ else if (eventName === "Stop" || eventName === "SubagentStop") { event = "assistant_message"; payload = { text: clip(lastAssistantText(p.transcript_path || ""), 4000) }; }
1964
+ else if (eventName === "SessionEnd") { event = "session_end"; payload = { reason: p.reason || null }; }
1965
+ else return;
1966
+
1967
+ const base = join((process.env.PROMETHEUS_DIR && process.env.PROMETHEUS_DIR !== "") ? process.env.PROMETHEUS_DIR : join(homedir(), ".prometheus"), "recorder", sanitize(projectId));
1968
+ const file = join(base, sanitize(sessionId) + ".jsonl");
1969
+
1970
+ // Per-session cap: once the spool is large, keep session_end + messages but
1971
+ // drop further tool_use (the noisiest, most numerous events).
1972
+ try {
1973
+ if (event === "tool_use") {
1974
+ let size = 0; try { size = statSync(file).size; } catch { size = 0; }
1975
+ if (size >= SPOOL_CAP) return;
1976
+ }
1977
+ } catch { /* stat failure -> keep going */ }
1978
+
1979
+ const line = JSON.stringify({ v: ${SPOOL_VERSION}, ts: new Date().toISOString(), projectId: projectId, sessionId: sessionId, agent: "claude-code", event: event, payload: JSON.parse(finalize(payload)) }) + "\n";
1980
+ try { mkdirSync(base, { recursive: true }); appendFileSync(file, line, { encoding: "utf8", flag: "a" }); } catch { /* never throw */ }
1981
+ }
1982
+
1983
+ if (process.stdin.isTTY) { process.exit(0); }
1984
+ else {
1985
+ let raw = "";
1986
+ process.stdin.on("data", function (c) { raw += c; });
1987
+ process.stdin.on("end", function () { try { run(raw); } catch { /* swallow */ } process.exit(0); });
1988
+ process.stdin.on("error", function () { process.exit(0); });
1989
+ }
1990
+ `;
1991
+ var RECORDER_EVENTS = [
1992
+ "SessionStart",
1993
+ "UserPromptSubmit",
1994
+ "PostToolUse",
1995
+ "Stop",
1996
+ "SessionEnd"
1997
+ ];
1998
+ var RECORDER_HOOK_TIMEOUT = 5;
1999
+ function resolveSettingsPath(opts) {
2000
+ if (opts.settingsPathOverride)
2001
+ return opts.settingsPathOverride;
2002
+ const root = opts.projectRoot ?? process.cwd();
2003
+ if (opts.scope === "project")
2004
+ return join4(root, ".claude", "settings.json");
2005
+ if (opts.scope === "project-local")
2006
+ return join4(root, ".claude", "settings.local.json");
2007
+ return join4(homedir4(), ".claude", "settings.json");
2008
+ }
2009
+ function resolveHookPath(opts) {
2010
+ const dir = opts.hookDirOverride ?? join4(homedir4(), ".prometheus", "hooks");
2011
+ return join4(dir, RECORDER_HOOK_FILENAME);
2012
+ }
2013
+ function ownsEntry(entry) {
2014
+ return Array.isArray(entry.hooks) && entry.hooks.some((h) => typeof h?.command === "string" && h.command.includes(RECORDER_HOOK_FILENAME));
2015
+ }
2016
+ function readSettings(settingsPath) {
2017
+ if (!existsSync(settingsPath))
2018
+ return {};
2019
+ const raw = readFileSync2(settingsPath, "utf8").replace(/^/, "");
2020
+ if (raw.trim() === "")
2021
+ return {};
2022
+ const parsed = JSON.parse(raw);
2023
+ if (typeof parsed !== "object" || parsed === null) {
2024
+ throw new Error(`${settingsPath} does not contain a JSON object.`);
2025
+ }
2026
+ return parsed;
2027
+ }
2028
+ function backupSettings(settingsPath) {
2029
+ if (!existsSync(settingsPath))
2030
+ return null;
2031
+ const d = /* @__PURE__ */ new Date();
2032
+ const p = (n) => String(n).padStart(2, "0");
2033
+ const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
2034
+ const bak = `${settingsPath}.prom-backup-${stamp}`;
2035
+ copyFileSync(settingsPath, bak);
2036
+ return bak;
2037
+ }
2038
+ function recorderStatus(opts = {}) {
2039
+ const settingsPath = resolveSettingsPath(opts);
2040
+ const hookPath = resolveHookPath(opts);
2041
+ let installedEvents = [];
2042
+ try {
2043
+ const settings = readSettings(settingsPath);
2044
+ for (const ev of RECORDER_EVENTS) {
2045
+ const arr = settings.hooks?.[ev];
2046
+ if (Array.isArray(arr) && arr.some(ownsEntry))
2047
+ installedEvents.push(ev);
2048
+ }
2049
+ } catch {
2050
+ installedEvents = [];
2051
+ }
2052
+ return {
2053
+ installed: installedEvents.length > 0,
2054
+ scope: opts.scope ?? "user",
2055
+ events: installedEvents,
2056
+ settingsPath,
2057
+ hookScriptPresent: existsSync(hookPath)
2058
+ };
2059
+ }
2060
+ function applyRecorderHooks(opts = {}) {
2061
+ const scope = opts.scope ?? "user";
2062
+ const settingsPath = resolveSettingsPath(opts);
2063
+ const hookPath = resolveHookPath(opts);
2064
+ const command = `node "${hookPath.replace(/\\/g, "/")}"`;
2065
+ const settings = readSettings(settingsPath);
2066
+ const backup = backupSettings(settingsPath);
2067
+ settings.hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
2068
+ for (const ev of RECORDER_EVENTS) {
2069
+ const arr = settings.hooks[ev];
2070
+ if (Array.isArray(arr)) {
2071
+ const kept = arr.filter((e) => !ownsEntry(e));
2072
+ if (kept.length === 0)
2073
+ delete settings.hooks[ev];
2074
+ else
2075
+ settings.hooks[ev] = kept;
2076
+ }
2077
+ }
2078
+ if (!opts.uninstall) {
2079
+ mkdirSync2(dirname2(hookPath), { recursive: true });
2080
+ writeFileSync2(hookPath, RECORDER_HOOK_SCRIPT, "utf8");
2081
+ for (const ev of RECORDER_EVENTS) {
2082
+ const matcher = ev === "PostToolUse" ? "*" : "";
2083
+ (settings.hooks[ev] ??= []).push({
2084
+ matcher,
2085
+ hooks: [{ type: "command", command, timeout: RECORDER_HOOK_TIMEOUT }]
2086
+ });
2087
+ }
2088
+ }
2089
+ if (settings.hooks && Object.keys(settings.hooks).length === 0)
2090
+ delete settings.hooks;
2091
+ mkdirSync2(dirname2(settingsPath), { recursive: true });
2092
+ writeFileSync2(settingsPath, `${JSON.stringify(settings, null, 2)}
2093
+ `, "utf8");
2094
+ JSON.parse(readFileSync2(settingsPath, "utf8"));
2095
+ return {
2096
+ action: opts.uninstall ? "uninstalled" : "installed",
2097
+ settingsPath,
2098
+ scope,
2099
+ hookPath,
2100
+ events: opts.uninstall ? [] : [...RECORDER_EVENTS],
2101
+ backup,
2102
+ note: opts.uninstall ? "Removed the Prometheus session recorder hooks. Reload your Claude Code window(s)." : "Installed the session recorder (opt-in). Reload / restart your Claude Code window(s). It records LOCALLY to ~/.prometheus/recorder; nothing is uploaded. Uninstall anytime with recorder_setup { uninstall: true }."
2103
+ };
2104
+ }
2105
+
1736
2106
  // dist/rrf.js
1737
2107
  function reciprocalRankFusion(lists, options = {}) {
1738
2108
  const k = options.k ?? 60;
@@ -1993,6 +2363,54 @@ CREATE TRIGGER IF NOT EXISTS agent_memory_vec_ad AFTER DELETE ON agent_memory BE
1993
2363
  DELETE FROM agent_memory_vec WHERE record_id = old.id;
1994
2364
  END;
1995
2365
  `;
2366
+ var RECORDER_SCHEMA = `
2367
+ CREATE TABLE IF NOT EXISTS agent_sessions (
2368
+ session_id TEXT NOT NULL,
2369
+ project_id TEXT NOT NULL,
2370
+ agent TEXT NOT NULL,
2371
+ cwd TEXT,
2372
+ model TEXT,
2373
+ started_at TEXT NOT NULL,
2374
+ ended_at TEXT,
2375
+ stats TEXT,
2376
+ summary TEXT,
2377
+ curated_at TEXT,
2378
+ PRIMARY KEY (project_id, session_id)
2379
+ );
2380
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON agent_sessions (project_id, started_at DESC);
2381
+
2382
+ CREATE TABLE IF NOT EXISTS session_events (
2383
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2384
+ project_id TEXT NOT NULL,
2385
+ session_id TEXT NOT NULL,
2386
+ seq INTEGER NOT NULL,
2387
+ ts TEXT NOT NULL,
2388
+ event_type TEXT NOT NULL,
2389
+ tool_name TEXT,
2390
+ content TEXT NOT NULL,
2391
+ metadata TEXT,
2392
+ UNIQUE (project_id, session_id, seq)
2393
+ );
2394
+ CREATE INDEX IF NOT EXISTS idx_events_session ON session_events (project_id, session_id, seq);
2395
+ `;
2396
+ var RECORDER_FTS_SCHEMA = `
2397
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_events_fts USING fts5(
2398
+ content, tool_name,
2399
+ content='session_events',
2400
+ content_rowid='id',
2401
+ tokenize='unicode61'
2402
+ );
2403
+ CREATE TRIGGER IF NOT EXISTS session_events_ai AFTER INSERT ON session_events BEGIN
2404
+ INSERT INTO session_events_fts (rowid, content, tool_name)
2405
+ VALUES (new.id, new.content, new.tool_name);
2406
+ END;
2407
+ CREATE TRIGGER IF NOT EXISTS session_events_ad AFTER DELETE ON session_events BEGIN
2408
+ INSERT INTO session_events_fts (session_events_fts, rowid, content, tool_name)
2409
+ VALUES ('delete', old.id, old.content, old.tool_name);
2410
+ END;
2411
+ `;
2412
+ var DEFAULT_RETENTION_DAYS = 90;
2413
+ var DEFAULT_MAX_SESSIONS = 300;
1996
2414
  function vectorToBlob(vector) {
1997
2415
  return Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
1998
2416
  }
@@ -2043,6 +2461,70 @@ function rowToRecord(row) {
2043
2461
  updatedAt: row.updated_at
2044
2462
  };
2045
2463
  }
2464
+ function sessionRowToRecord(r) {
2465
+ return {
2466
+ sessionId: r.session_id,
2467
+ projectId: r.project_id,
2468
+ agent: r.agent,
2469
+ cwd: r.cwd,
2470
+ model: r.model,
2471
+ startedAt: r.started_at,
2472
+ endedAt: r.ended_at,
2473
+ stats: r.stats ? JSON.parse(r.stats) : null,
2474
+ summary: r.summary,
2475
+ curatedAt: r.curated_at
2476
+ };
2477
+ }
2478
+ function strField(payload, key) {
2479
+ const v = payload[key];
2480
+ return typeof v === "string" ? v : null;
2481
+ }
2482
+ function recorderContent(ev) {
2483
+ let content;
2484
+ let toolName = null;
2485
+ if (ev.event === "user_message" || ev.event === "assistant_message") {
2486
+ content = strField(ev.payload, "text") ?? "";
2487
+ } else if (ev.event === "tool_use") {
2488
+ toolName = strField(ev.payload, "tool");
2489
+ content = JSON.stringify(ev.payload);
2490
+ } else {
2491
+ content = JSON.stringify(ev.payload);
2492
+ }
2493
+ if (findSecretPatterns(content).length > 0)
2494
+ content = "[dropped: secret-like]";
2495
+ return { content, toolName };
2496
+ }
2497
+ function computeSessionStats(evs) {
2498
+ let toolCount = 0;
2499
+ const tools = /* @__PURE__ */ new Set();
2500
+ const files = /* @__PURE__ */ new Set();
2501
+ for (const ev of evs) {
2502
+ if (ev.event !== "tool_use")
2503
+ continue;
2504
+ toolCount++;
2505
+ const t = strField(ev.payload, "tool");
2506
+ if (t !== null)
2507
+ tools.add(t);
2508
+ const fp = strField(ev.payload, "file_path");
2509
+ if (fp !== null)
2510
+ files.add(fp);
2511
+ }
2512
+ return { toolCount, toolsUsed: [...tools], filesTouched: [...files] };
2513
+ }
2514
+ function condenseEvent(r) {
2515
+ let content = r.content;
2516
+ if (r.event_type === "tool_use") {
2517
+ content = r.tool_name ? `[tool] ${r.tool_name}: ${r.content}` : `[tool] ${r.content}`;
2518
+ }
2519
+ return { seq: r.seq, ts: r.ts, eventType: r.event_type, toolName: r.tool_name, content };
2520
+ }
2521
+ function intFromEnv(env, name, def) {
2522
+ const raw = env[name];
2523
+ if (raw === void 0 || raw === "")
2524
+ return def;
2525
+ const n = Number.parseInt(raw, 10);
2526
+ return Number.isFinite(n) && n > 0 ? n : def;
2527
+ }
2046
2528
  var SqliteMemoryBackend = class {
2047
2529
  db;
2048
2530
  embedder;
@@ -2057,7 +2539,7 @@ var SqliteMemoryBackend = class {
2057
2539
  closed = false;
2058
2540
  constructor(dbPath, opts = {}) {
2059
2541
  if (dbPath !== ":memory:") {
2060
- mkdirSync2(dirname2(dbPath), { recursive: true });
2542
+ mkdirSync3(dirname3(dbPath), { recursive: true });
2061
2543
  }
2062
2544
  this.db = new Database(dbPath);
2063
2545
  this.db.pragma("journal_mode = WAL");
@@ -2065,6 +2547,8 @@ var SqliteMemoryBackend = class {
2065
2547
  this.db.exec(SCHEMA);
2066
2548
  this.db.exec(FTS_SCHEMA);
2067
2549
  this.db.exec(VEC_SCHEMA);
2550
+ this.db.exec(RECORDER_SCHEMA);
2551
+ this.db.exec(RECORDER_FTS_SCHEMA);
2068
2552
  this.db.exec(`INSERT INTO agent_memory_fts (agent_memory_fts) VALUES ('rebuild')`);
2069
2553
  this.embedder = opts.embedder;
2070
2554
  this.reranker = opts.reranker;
@@ -2492,6 +2976,183 @@ ${h.record.value}`
2492
2976
  this.audit("consolidate", { scope: input.scope, scopeId: input.scopeId }, `records=${written.length}`);
2493
2977
  return written;
2494
2978
  }
2979
+ // ===================================================================
2980
+ // Session Recorder (M1) — RecorderStore implementation.
2981
+ // ===================================================================
2982
+ /**
2983
+ * Ingest every spool file for `projectId` under the recorder root: parse,
2984
+ * write events idempotently (`INSERT OR IGNORE` on `(project_id, session_id,
2985
+ * seq)`), maintain the session header, and DELETE the spool of any session that
2986
+ * has ended (its `session_end` is persisted). Best-effort per file — a corrupt
2987
+ * spool never aborts the sweep. Returns roll-up counts.
2988
+ */
2989
+ async ingestSpoolDir(projectId, env = process.env) {
2990
+ const dir = join5(recorderRoot(env), sanitizeSegment(projectId));
2991
+ let sessions = 0;
2992
+ let events = 0;
2993
+ let deletedSpools = 0;
2994
+ let files;
2995
+ try {
2996
+ files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl"));
2997
+ } catch {
2998
+ return { sessions: 0, events: 0, deletedSpools: 0 };
2999
+ }
3000
+ for (const file of files) {
3001
+ const full = join5(dir, file);
3002
+ try {
3003
+ const raw = readFileSync3(full, "utf8");
3004
+ const parsed = parseSpool(raw);
3005
+ if (parsed.events.length === 0) {
3006
+ continue;
3007
+ }
3008
+ const written = this.ingestSessionEvents(projectId, parsed.events);
3009
+ events += written;
3010
+ sessions += 1;
3011
+ if (parsed.ended) {
3012
+ try {
3013
+ rmSync2(full, { force: true });
3014
+ deletedSpools += 1;
3015
+ } catch {
3016
+ }
3017
+ }
3018
+ } catch {
3019
+ }
3020
+ }
3021
+ return { sessions, events, deletedSpools };
3022
+ }
3023
+ /**
3024
+ * Write one session's parsed events: ensure the header, insert events
3025
+ * idempotently (re-scanning each content for secrets → `[dropped]`), and set
3026
+ * cwd/model (from `session_start`) + ended_at/stats (from `session_end`).
3027
+ * Runs in a single transaction. Returns the number of NEW event rows.
3028
+ */
3029
+ ingestSessionEvents(projectId, evs) {
3030
+ if (evs.length === 0)
3031
+ return 0;
3032
+ const sessionId = evs[0].sessionId;
3033
+ const agent = evs[0].agent || "claude-code";
3034
+ const firstTs = evs[0].ts;
3035
+ const ensureHeader = this.db.prepare(`INSERT OR IGNORE INTO agent_sessions (session_id, project_id, agent, started_at)
3036
+ VALUES (?, ?, ?, ?)`);
3037
+ const insertEvent = this.db.prepare(`INSERT OR IGNORE INTO session_events
3038
+ (project_id, session_id, seq, ts, event_type, tool_name, content, metadata)
3039
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
3040
+ const setStart = this.db.prepare(`UPDATE agent_sessions SET cwd = ?, model = ?, started_at = ? WHERE project_id = ? AND session_id = ?`);
3041
+ const setEnd = this.db.prepare(`UPDATE agent_sessions SET ended_at = ?, stats = ? WHERE project_id = ? AND session_id = ?`);
3042
+ let written = 0;
3043
+ const tx = this.db.transaction(() => {
3044
+ ensureHeader.run(sessionId, projectId, agent, firstTs);
3045
+ for (const ev of evs) {
3046
+ const { content, toolName } = recorderContent(ev);
3047
+ const res = insertEvent.run(projectId, sessionId, ev.seq, ev.ts, ev.event, toolName, content, null);
3048
+ written += res.changes;
3049
+ if (ev.event === "session_start") {
3050
+ const cwd = strField(ev.payload, "cwd");
3051
+ const model = strField(ev.payload, "model");
3052
+ setStart.run(cwd, model, ev.ts, projectId, sessionId);
3053
+ }
3054
+ }
3055
+ const end = evs.find((e) => e.event === "session_end");
3056
+ if (end !== void 0) {
3057
+ setEnd.run(end.ts, JSON.stringify(computeSessionStats(evs)), projectId, sessionId);
3058
+ }
3059
+ });
3060
+ tx();
3061
+ return written;
3062
+ }
3063
+ async listSessions(projectId, limit = 20) {
3064
+ const rows = this.db.prepare(`SELECT * FROM agent_sessions WHERE project_id = ? ORDER BY started_at DESC LIMIT ?`).all(projectId, limit);
3065
+ return rows.map(sessionRowToRecord);
3066
+ }
3067
+ async getSession(projectId, sessionId, maxChars = 2e4) {
3068
+ const header = this.db.prepare(`SELECT * FROM agent_sessions WHERE project_id = ? AND session_id = ?`).get(projectId, sessionId);
3069
+ if (header === void 0)
3070
+ return null;
3071
+ const rows = this.db.prepare(`SELECT seq, ts, event_type, tool_name, content FROM session_events
3072
+ WHERE project_id = ? AND session_id = ? ORDER BY seq ASC`).all(projectId, sessionId);
3073
+ const events = [];
3074
+ let used = 0;
3075
+ let truncated = false;
3076
+ for (const r of rows) {
3077
+ const line = condenseEvent(r);
3078
+ if (used + line.content.length > maxChars && events.length > 0) {
3079
+ truncated = true;
3080
+ break;
3081
+ }
3082
+ used += line.content.length;
3083
+ events.push(line);
3084
+ }
3085
+ return { session: sessionRowToRecord(header), events, truncated };
3086
+ }
3087
+ async searchSessions(projectId, query, limit = 20) {
3088
+ const match = toFtsQuery(query);
3089
+ if (match === "")
3090
+ return [];
3091
+ const rows = this.db.prepare(`SELECT e.session_id AS session_id, e.ts AS ts, e.event_type AS event_type,
3092
+ snippet(session_events_fts, 0, '\xAB', '\xBB', ' \u2026 ', 12) AS snip
3093
+ FROM session_events_fts f
3094
+ JOIN session_events e ON e.id = f.rowid
3095
+ WHERE session_events_fts MATCH ? AND e.project_id = ?
3096
+ ORDER BY rank LIMIT ?`).all(match, projectId, limit);
3097
+ return rows.map((r) => ({
3098
+ sessionId: r.session_id,
3099
+ ts: r.ts,
3100
+ eventType: r.event_type,
3101
+ snippet: r.snip
3102
+ }));
3103
+ }
3104
+ /** M2: sessions that ended but have not been curated yet (newest first). */
3105
+ async listUncuratedSessions(projectId, limit = 10) {
3106
+ const rows = this.db.prepare(`SELECT * FROM agent_sessions
3107
+ WHERE project_id = ? AND ended_at IS NOT NULL AND curated_at IS NULL
3108
+ ORDER BY started_at DESC LIMIT ?`).all(projectId, limit);
3109
+ return rows.map(sessionRowToRecord);
3110
+ }
3111
+ /** M2: store a session's distilled summary + stamp `curated_at` (idempotency gate). */
3112
+ async setCuration(projectId, sessionId, summary) {
3113
+ this.db.prepare(`UPDATE agent_sessions SET summary = ?, curated_at = ? WHERE project_id = ? AND session_id = ?`).run(summary, (/* @__PURE__ */ new Date()).toISOString(), projectId, sessionId);
3114
+ }
3115
+ async recorderTotals(projectId) {
3116
+ const s = this.db.prepare(`SELECT COUNT(*) AS n FROM agent_sessions WHERE project_id = ?`).get(projectId);
3117
+ const e = this.db.prepare(`SELECT COUNT(*) AS n FROM session_events WHERE project_id = ?`).get(projectId);
3118
+ return { sessions: s.n, events: e.n };
3119
+ }
3120
+ /**
3121
+ * Apply retention (spec §3.7): keep at most `maxSessions` newest sessions per
3122
+ * project (older sessions + their events are dropped), and drop events older
3123
+ * than `retentionDays`. Curated sessions (`curated_at` set — M2) older than 14
3124
+ * days keep only their header+summary (events dropped). Env overrides:
3125
+ * `PROMETHEUS_RECORDER_RETENTION_DAYS` / `PROMETHEUS_RECORDER_MAX_SESSIONS`.
3126
+ */
3127
+ async applyRecorderRetention(env = process.env) {
3128
+ const days = intFromEnv(env, "PROMETHEUS_RECORDER_RETENTION_DAYS", DEFAULT_RETENTION_DAYS);
3129
+ const maxSessions = intFromEnv(env, "PROMETHEUS_RECORDER_MAX_SESSIONS", DEFAULT_MAX_SESSIONS);
3130
+ const cutoff = new Date(Date.now() - days * 864e5).toISOString();
3131
+ const curatedCutoff = new Date(Date.now() - 14 * 864e5).toISOString();
3132
+ let prunedSessions = 0;
3133
+ let prunedEvents = 0;
3134
+ const tx = this.db.transaction(() => {
3135
+ const overflow = this.db.prepare(`SELECT project_id, session_id FROM (
3136
+ SELECT project_id, session_id,
3137
+ ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY started_at DESC) AS rn
3138
+ FROM agent_sessions
3139
+ ) WHERE rn > ?`).all(maxSessions);
3140
+ const delEvents = this.db.prepare(`DELETE FROM session_events WHERE project_id = ? AND session_id = ?`);
3141
+ const delSession = this.db.prepare(`DELETE FROM agent_sessions WHERE project_id = ? AND session_id = ?`);
3142
+ for (const o of overflow) {
3143
+ prunedEvents += delEvents.run(o.project_id, o.session_id).changes;
3144
+ prunedSessions += delSession.run(o.project_id, o.session_id).changes;
3145
+ }
3146
+ prunedEvents += this.db.prepare(`DELETE FROM session_events WHERE ts < ?`).run(cutoff).changes;
3147
+ const curated = this.db.prepare(`SELECT project_id, session_id FROM agent_sessions
3148
+ WHERE curated_at IS NOT NULL AND curated_at < ?`).all(curatedCutoff);
3149
+ for (const c of curated) {
3150
+ prunedEvents += delEvents.run(c.project_id, c.session_id).changes;
3151
+ }
3152
+ });
3153
+ tx();
3154
+ return { prunedSessions, prunedEvents };
3155
+ }
2495
3156
  async close() {
2496
3157
  if (this.closed)
2497
3158
  return;
@@ -2510,7 +3171,7 @@ function projectIdFor(workspaceRoot) {
2510
3171
  return createHash("sha256").update(abs).digest("hex").slice(0, 16);
2511
3172
  }
2512
3173
  function defaultMemoryDbPath() {
2513
- return join4(homedir4(), ".prometheus", "memory.db");
3174
+ return join6(homedir5(), ".prometheus", "memory.db");
2514
3175
  }
2515
3176
  function intEnv(env, name, def) {
2516
3177
  const raw = env[name];
@@ -2778,6 +3439,7 @@ function composeFromEnv(opts) {
2778
3439
  });
2779
3440
  return {
2780
3441
  backend,
3442
+ recorder: backend,
2781
3443
  workspaceRoot,
2782
3444
  projectId,
2783
3445
  projectName,
@@ -2799,47 +3461,57 @@ function composeFromEnv(opts) {
2799
3461
  };
2800
3462
  }
2801
3463
 
2802
- // dist/roots.js
2803
- import { fileURLToPath as fileURLToPath2 } from "node:url";
2804
- async function rootFromClient(server, timeoutMs = 2500) {
2805
- let supportsRoots = false;
2806
- try {
2807
- supportsRoots = server.getClientCapabilities()?.roots != null;
2808
- } catch {
2809
- return null;
2810
- }
2811
- if (!supportsRoots)
2812
- return null;
2813
- let res;
2814
- try {
2815
- res = await server.listRoots(void 0, { timeout: timeoutMs });
2816
- } catch {
2817
- return null;
2818
- }
2819
- const roots = res?.roots ?? [];
2820
- for (const r of roots) {
2821
- const uri = typeof r?.uri === "string" ? r.uri : "";
2822
- if (uri.startsWith("file://")) {
2823
- try {
2824
- return fileURLToPath2(uri);
2825
- } catch {
2826
- }
2827
- }
2828
- }
2829
- return null;
2830
- }
2831
-
2832
- // dist/server.js
2833
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2834
-
2835
- // dist/tools.js
2836
- import { z } from "zod";
2837
-
2838
3464
  // dist/project-files.js
3465
+ import { execFileSync } from "node:child_process";
2839
3466
  import * as fs from "node:fs/promises";
2840
3467
  import * as path from "node:path";
2841
3468
  var MEMORIES_DIR = path.join(".prometheus", "memories");
2842
3469
  var PROJECT_FILE_SOURCE = "import:project-file";
3470
+ var VALID_TYPES = /* @__PURE__ */ new Set(["semantic", "procedural", "episodic", "working"]);
3471
+ function serializeMemoryFile(f) {
3472
+ const lines = ["---", `type: ${f.type}`];
3473
+ if (f.tags && f.tags.length > 0)
3474
+ lines.push(`tags: [${f.tags.join(", ")}]`);
3475
+ if (f.confidence !== void 0)
3476
+ lines.push(`confidence: ${f.confidence}`);
3477
+ lines.push(`updated: ${f.updated ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
3478
+ if (f.source !== void 0 && f.source !== "")
3479
+ lines.push(`source: ${f.source}`);
3480
+ lines.push("---", "", f.value.replace(/\s+$/, ""), "");
3481
+ return lines.join("\n");
3482
+ }
3483
+ function parseMemoryFile(content) {
3484
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(content);
3485
+ if (m === null)
3486
+ return { type: "semantic", tags: void 0, confidence: void 0, source: void 0, body: content.trim() };
3487
+ const fm = m[1] ?? "";
3488
+ const body = content.slice(m[0].length).replace(/^\r?\n/, "").trim();
3489
+ let type = "semantic";
3490
+ let tags;
3491
+ let confidence;
3492
+ let source;
3493
+ for (const line of fm.split(/\r?\n/)) {
3494
+ const kv = /^([A-Za-z_]+)\s*:\s*(.*)$/.exec(line.trim());
3495
+ if (kv === null)
3496
+ continue;
3497
+ const key = kv[1].toLowerCase();
3498
+ const val = kv[2].trim();
3499
+ if (key === "type" && VALID_TYPES.has(val))
3500
+ type = val;
3501
+ else if (key === "tags") {
3502
+ const inner = val.replace(/^\[|\]$/g, "");
3503
+ const parsed = inner.split(",").map((t) => t.trim().replace(/^["']|["']$/g, "")).filter((t) => t !== "");
3504
+ if (parsed.length > 0)
3505
+ tags = parsed;
3506
+ } else if (key === "confidence") {
3507
+ const n = Number.parseFloat(val);
3508
+ if (Number.isFinite(n) && n >= 0 && n <= 1)
3509
+ confidence = n;
3510
+ } else if (key === "source" && val !== "")
3511
+ source = val;
3512
+ }
3513
+ return { type, tags, confidence, source, body };
3514
+ }
2843
3515
  function memoriesDir(workspaceRoot) {
2844
3516
  return path.join(workspaceRoot, MEMORIES_DIR);
2845
3517
  }
@@ -2853,13 +3525,25 @@ function keyToFilename(key) {
2853
3525
  function filenameToKey(filename) {
2854
3526
  return filename.replace(/\.md$/i, "");
2855
3527
  }
2856
- async function writeProjectFile(workspaceRoot, key, content) {
3528
+ async function writeMemoryFile(workspaceRoot, f) {
2857
3529
  const dir = memoriesDir(workspaceRoot);
2858
3530
  await fs.mkdir(dir, { recursive: true });
2859
- const file = path.join(dir, keyToFilename(key));
2860
- await fs.writeFile(file, content, "utf-8");
3531
+ const file = path.join(dir, keyToFilename(f.key));
3532
+ await fs.writeFile(file, serializeMemoryFile(f), "utf-8");
2861
3533
  return file;
2862
3534
  }
3535
+ function memoriesShared(workspaceRoot) {
3536
+ const probe = path.join(MEMORIES_DIR, ".prom-shared-probe");
3537
+ try {
3538
+ execFileSync("git", ["-C", workspaceRoot, "check-ignore", "-q", probe], {
3539
+ stdio: ["ignore", "ignore", "ignore"],
3540
+ timeout: 3e3
3541
+ });
3542
+ return false;
3543
+ } catch (err) {
3544
+ return err.status === 1;
3545
+ }
3546
+ }
2863
3547
  async function deleteProjectFile(workspaceRoot, key) {
2864
3548
  const file = path.join(memoriesDir(workspaceRoot), keyToFilename(key));
2865
3549
  try {
@@ -2893,67 +3577,196 @@ async function listProjectFiles(workspaceRoot) {
2893
3577
  async function syncProjectFiles(backend, input) {
2894
3578
  const files = await listProjectFiles(input.workspaceRoot);
2895
3579
  const scopeId = input.scopeId ?? input.projectId;
3580
+ const fileKeys = /* @__PURE__ */ new Set();
3581
+ const skippedKeys = /* @__PURE__ */ new Set();
3582
+ const skipped = [];
3583
+ let synced = 0;
2896
3584
  for (const file of files) {
3585
+ const parsed = parseMemoryFile(file.content);
3586
+ if (findSecretPatterns(parsed.body).length > 0) {
3587
+ skipped.push({ key: file.key, reason: "secret-like content" });
3588
+ skippedKeys.add(file.key);
3589
+ continue;
3590
+ }
3591
+ fileKeys.add(file.key);
2897
3592
  await backend.write({
2898
3593
  projectId: input.projectId,
2899
3594
  scope: "project",
2900
3595
  scopeId,
2901
- type: "semantic",
3596
+ type: parsed.type,
2902
3597
  key: file.key,
2903
- value: file.content,
3598
+ value: parsed.body,
3599
+ ...parsed.tags !== void 0 ? { tags: parsed.tags } : {},
3600
+ ...parsed.confidence !== void 0 ? { confidence: parsed.confidence } : {},
2904
3601
  source: PROJECT_FILE_SOURCE
2905
3602
  });
3603
+ synced++;
2906
3604
  }
2907
- return files.length;
3605
+ let pruned = 0;
3606
+ const existing = await backend.list({ projectId: input.projectId, scope: "project" });
3607
+ for (const rec of existing) {
3608
+ if (rec.source !== PROJECT_FILE_SOURCE)
3609
+ continue;
3610
+ if (fileKeys.has(rec.key) || skippedKeys.has(rec.key))
3611
+ continue;
3612
+ await backend.delete({
3613
+ projectId: input.projectId,
3614
+ scope: "project",
3615
+ scopeId,
3616
+ type: rec.type,
3617
+ key: rec.key
3618
+ });
3619
+ pruned++;
3620
+ }
3621
+ return { synced, pruned, skipped };
2908
3622
  }
2909
3623
 
2910
- // dist/security.js
2911
- var SECRET_PATTERNS = [
2912
- { name: "openai-key", regex: /\bsk-proj-[A-Za-z0-9_-]{20,}/ },
2913
- { name: "anthropic-key", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
2914
- { name: "supabase-token", regex: /\bsbp?_[A-Za-z0-9]{20,}/ },
2915
- { name: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}/ },
2916
- { name: "gitlab-token", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
2917
- { name: "dockerhub-token", regex: /\bdckr_(?:pat|oat)_[A-Za-z0-9_-]{10,}/ },
2918
- { name: "resend-key", regex: /\bre_[A-Za-z0-9]{8,}_[A-Za-z0-9]{10,}/ },
2919
- { name: "runpod-key", regex: /\brpa_[A-Za-z0-9]{30,}/ },
2920
- { name: "sentry-token", regex: /\bsntrys_[A-Za-z0-9+/=_-]{20,}/ },
2921
- { name: "vercel-token", regex: /\bvc[kp]_[A-Za-z0-9]{20,}/ },
2922
- { name: "huggingface-token", regex: /\bhf_[A-Za-z0-9]{30,}/ },
2923
- { name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{30,}/ },
2924
- { name: "voyage-key", regex: /\bpa-[A-Za-z0-9_-]{30,}/ },
2925
- { name: "google-api-key", regex: /\bAIza[A-Za-z0-9_-]{30,}/ },
2926
- { name: "sovrgpt-key", regex: /\bsov_[a-f0-9]{40,}/ },
2927
- { name: "prometheus-key", regex: /\bprom_(?:live|test)_[A-Za-z0-9]{10,}/ },
2928
- { name: "aws-access-key", regex: /\bAKIA[A-Z0-9]{16}\b/ },
2929
- { name: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/ },
2930
- { name: "private-key-block", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
2931
- { name: "authorization-header", regex: /\bAuthorization:\s*(?:Bearer|Basic)\s+\S{8,}/i },
2932
- {
2933
- name: "connection-string-credentials",
2934
- regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s/@:]+:[^\s/@]+@/i
3624
+ // dist/curation.js
3625
+ function sessionLog(events) {
3626
+ const lines = [];
3627
+ for (const e of events) {
3628
+ if (e.eventType === "user_message")
3629
+ lines.push(`USER: ${e.content}`);
3630
+ else if (e.eventType === "assistant_message")
3631
+ lines.push(`ASSISTANT: ${e.content}`);
3632
+ else if (e.eventType === "tool_use")
3633
+ lines.push(e.content);
2935
3634
  }
2936
- ];
2937
- function findSecretPatterns(text) {
2938
- const hits = [];
2939
- for (const p of SECRET_PATTERNS) {
2940
- if (p.regex.test(text))
2941
- hits.push(p.name);
3635
+ return lines.join("\n");
3636
+ }
3637
+ async function curateSession(deps, sessionId) {
3638
+ const { backend, recorder, extractor, projectId, workspaceRoot, mirrorToFiles } = deps;
3639
+ const detail = await recorder.getSession(projectId, sessionId, 4e4);
3640
+ if (detail === null)
3641
+ return { curated: false, reason: "no such session" };
3642
+ const log = sessionLog(detail.events);
3643
+ if (log.trim() === "") {
3644
+ await recorder.setCuration(projectId, sessionId, "");
3645
+ return { curated: true, summary: "", facts: 0, procedures: 0 };
2942
3646
  }
2943
- return hits;
3647
+ let result;
3648
+ try {
3649
+ result = await extractor.curate(log);
3650
+ } catch {
3651
+ return { curated: false, reason: "curation call failed" };
3652
+ }
3653
+ if (result.summary.trim() === "") {
3654
+ return { curated: false, reason: "curator returned no summary (provider unavailable?)" };
3655
+ }
3656
+ const scope = "project";
3657
+ const scopeId = scopeIdFor(scope, projectId);
3658
+ const source = `curated:session:${sessionId}`;
3659
+ const summary = findSecretPatterns(result.summary).length > 0 ? "[dropped: secret-like]" : result.summary;
3660
+ await recorder.setCuration(projectId, sessionId, summary);
3661
+ await backend.write({
3662
+ projectId,
3663
+ scope,
3664
+ scopeId,
3665
+ type: "episodic",
3666
+ key: `session:${sessionId}`,
3667
+ value: summary,
3668
+ source
3669
+ });
3670
+ let factCount = 0;
3671
+ for (const f of result.facts) {
3672
+ try {
3673
+ assertNoSecrets(`${f.key}
3674
+ ${f.value}`);
3675
+ } catch {
3676
+ continue;
3677
+ }
3678
+ await backend.write({
3679
+ projectId,
3680
+ scope,
3681
+ scopeId,
3682
+ type: "semantic",
3683
+ key: f.key,
3684
+ value: f.value,
3685
+ ...f.confidence !== void 0 ? { confidence: f.confidence } : {},
3686
+ source
3687
+ });
3688
+ if (mirrorToFiles) {
3689
+ try {
3690
+ await writeMemoryFile(workspaceRoot, {
3691
+ key: f.key,
3692
+ type: "semantic",
3693
+ value: f.value,
3694
+ ...f.confidence !== void 0 ? { confidence: f.confidence } : {},
3695
+ source
3696
+ });
3697
+ } catch {
3698
+ }
3699
+ }
3700
+ factCount++;
3701
+ }
3702
+ let procCount = 0;
3703
+ for (const p of result.procedures) {
3704
+ try {
3705
+ assertNoSecrets(`${p.key}
3706
+ ${p.value}`);
3707
+ } catch {
3708
+ continue;
3709
+ }
3710
+ await backend.write({
3711
+ projectId,
3712
+ scope,
3713
+ scopeId,
3714
+ type: "procedural",
3715
+ key: p.key,
3716
+ value: p.value,
3717
+ source
3718
+ });
3719
+ if (mirrorToFiles) {
3720
+ try {
3721
+ await writeMemoryFile(workspaceRoot, { key: p.key, type: "procedural", value: p.value, source });
3722
+ } catch {
3723
+ }
3724
+ }
3725
+ procCount++;
3726
+ }
3727
+ return { curated: true, summary, facts: factCount, procedures: procCount };
2944
3728
  }
2945
- var SECRET_VALUE_ERROR = "memory value matches the secret deny-list and was rejected";
2946
- function assertNoSecrets(text) {
2947
- const hits = findSecretPatterns(text);
2948
- if (hits.length > 0) {
2949
- throw new Error(`${SECRET_VALUE_ERROR} (pattern: ${hits.join(", ")}).`);
3729
+
3730
+ // dist/roots.js
3731
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
3732
+ async function rootFromClient(server, timeoutMs = 2500) {
3733
+ let supportsRoots = false;
3734
+ try {
3735
+ supportsRoots = server.getClientCapabilities()?.roots != null;
3736
+ } catch {
3737
+ return null;
3738
+ }
3739
+ if (!supportsRoots)
3740
+ return null;
3741
+ let res;
3742
+ try {
3743
+ res = await server.listRoots(void 0, { timeout: timeoutMs });
3744
+ } catch {
3745
+ return null;
2950
3746
  }
3747
+ const roots = res?.roots ?? [];
3748
+ for (const r of roots) {
3749
+ const uri = typeof r?.uri === "string" ? r.uri : "";
3750
+ if (uri.startsWith("file://")) {
3751
+ try {
3752
+ return fileURLToPath2(uri);
3753
+ } catch {
3754
+ }
3755
+ }
3756
+ }
3757
+ return null;
2951
3758
  }
2952
3759
 
3760
+ // dist/server.js
3761
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3762
+
3763
+ // dist/tools.js
3764
+ import { z } from "zod";
3765
+
2953
3766
  // dist/setup.js
2954
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
3767
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "node:fs";
2955
3768
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
2956
- import { dirname as dirname3, join as join6 } from "node:path";
3769
+ import { dirname as dirname4, join as join8 } from "node:path";
2957
3770
  var MEMORY_RUNTIMES = [
2958
3771
  "claude-code",
2959
3772
  "cursor",
@@ -2997,13 +3810,13 @@ alwaysApply: true
2997
3810
  var TARGETS = {
2998
3811
  "claude-code": { relPath: "CLAUDE.md", mode: "block", detect: "CLAUDE.md" },
2999
3812
  cursor: {
3000
- relPath: join6(".cursor", "rules", "prometheus-memory.mdc"),
3813
+ relPath: join8(".cursor", "rules", "prometheus-memory.mdc"),
3001
3814
  mode: "file",
3002
3815
  fileContent: CURSOR_FRONTMATTER + withMarkers(RULE_BLOCK) + "\n",
3003
3816
  detect: ".cursor"
3004
3817
  },
3005
3818
  augment: {
3006
- relPath: join6(".augment", "rules", "prometheus-memory.md"),
3819
+ relPath: join8(".augment", "rules", "prometheus-memory.md"),
3007
3820
  mode: "file",
3008
3821
  fileContent: withMarkers(RULE_BLOCK) + "\n",
3009
3822
  detect: ".augment"
@@ -3011,19 +3824,19 @@ var TARGETS = {
3011
3824
  agents: { relPath: "AGENTS.md", mode: "block", detect: "AGENTS.md" }
3012
3825
  };
3013
3826
  function detectRuntimes(workspaceRoot) {
3014
- const found = MEMORY_RUNTIMES.filter((rt) => existsSync(join6(workspaceRoot, TARGETS[rt].detect)));
3827
+ const found = MEMORY_RUNTIMES.filter((rt) => existsSync2(join8(workspaceRoot, TARGETS[rt].detect)));
3015
3828
  return found.length > 0 ? found : ["agents"];
3016
3829
  }
3017
3830
  function existingRuntimes(workspaceRoot) {
3018
- return MEMORY_RUNTIMES.filter((rt) => existsSync(join6(workspaceRoot, TARGETS[rt].detect)));
3831
+ return MEMORY_RUNTIMES.filter((rt) => existsSync2(join8(workspaceRoot, TARGETS[rt].detect)));
3019
3832
  }
3020
3833
  function installedRuntimes(workspaceRoot) {
3021
3834
  return MEMORY_RUNTIMES.filter((rt) => {
3022
- const p = join6(workspaceRoot, TARGETS[rt].relPath);
3023
- if (!existsSync(p))
3835
+ const p = join8(workspaceRoot, TARGETS[rt].relPath);
3836
+ if (!existsSync2(p))
3024
3837
  return false;
3025
3838
  try {
3026
- return readFileSync2(p, "utf-8").includes(BLOCK_START);
3839
+ return readFileSync4(p, "utf-8").includes(BLOCK_START);
3027
3840
  } catch {
3028
3841
  return false;
3029
3842
  }
@@ -3048,14 +3861,14 @@ function upsertBlock(existing, block) {
3048
3861
  }
3049
3862
  async function installRuntime(workspaceRoot, runtime) {
3050
3863
  const target = TARGETS[runtime];
3051
- const absPath = join6(workspaceRoot, target.relPath);
3052
- const exists = existsSync(absPath);
3864
+ const absPath = join8(workspaceRoot, target.relPath);
3865
+ const exists = existsSync2(absPath);
3053
3866
  const before = exists ? await readFile3(absPath, "utf-8") : "";
3054
3867
  const after = target.mode === "file" ? target.fileContent : upsertBlock(before, RULE_BLOCK);
3055
3868
  if (exists && before === after) {
3056
3869
  return { runtime, path: absPath, action: "unchanged" };
3057
3870
  }
3058
- await mkdir3(dirname3(absPath), { recursive: true });
3871
+ await mkdir3(dirname4(absPath), { recursive: true });
3059
3872
  await writeFile3(absPath, after, "utf-8");
3060
3873
  return { runtime, path: absPath, action: exists ? "updated" : "created" };
3061
3874
  }
@@ -3143,6 +3956,9 @@ function recordToJson(rec) {
3143
3956
  value: rec.value,
3144
3957
  confidence: rec.confidence ?? null,
3145
3958
  source: rec.source ?? null,
3959
+ // M3-git: where this record came from — a committed/shared file, or the
3960
+ // local DB only (a private write or a not-yet-shared record).
3961
+ origin: rec.source === PROJECT_FILE_SOURCE ? "file" : "local",
3146
3962
  tags: rec.tags ?? [],
3147
3963
  useCount: rec.useCount,
3148
3964
  createdAt: rec.createdAt,
@@ -3174,7 +3990,13 @@ var writeInput = {
3174
3990
  key: z.string().min(1, "key must not be empty"),
3175
3991
  value: z.string().min(1, "value must not be empty"),
3176
3992
  confidence: z.number().min(0).max(1).optional(),
3177
- tags: z.array(z.string().min(1)).optional()
3993
+ tags: z.array(z.string().min(1)).optional(),
3994
+ /**
3995
+ * M3-git: promote this record to a committed `.prometheus/memories/` file so
3996
+ * it is SHARED with the team via git (file = shared, DB = private). Default
3997
+ * false (DB-only). Only project-scope semantic/procedural records are shareable.
3998
+ */
3999
+ share: z.boolean().optional()
3178
4000
  };
3179
4001
  var captureInput = {
3180
4002
  sessionId: z.string().min(1, "sessionId must not be empty"),
@@ -3207,12 +4029,31 @@ var deleteInput = {
3207
4029
  var searchInput = {
3208
4030
  query: z.string().min(1, "query must not be empty"),
3209
4031
  types: z.array(typeEnum).min(1).optional(),
3210
- limit: z.number().int().positive().max(MAX_LIMIT).optional()
4032
+ limit: z.number().int().positive().max(MAX_LIMIT).optional(),
4033
+ /**
4034
+ * Which stores to search. Default `["memories"]` (unchanged behaviour).
4035
+ * `"sessions"` also searches the Session Recorder's event log (M1).
4036
+ */
4037
+ sources: z.array(z.enum(["memories", "sessions"])).min(1).optional()
3211
4038
  };
3212
4039
  var runtimeEnum = z.enum(MEMORY_RUNTIMES);
3213
4040
  var setupInput = {
3214
4041
  runtimes: z.array(runtimeEnum).min(1).optional()
3215
4042
  };
4043
+ var recorderSetupInput = {
4044
+ scope: z.enum(["user", "project", "project-local"]).optional(),
4045
+ uninstall: z.boolean().optional()
4046
+ };
4047
+ var sessionsInput = {
4048
+ mode: z.enum(["list", "get"]).optional(),
4049
+ sessionId: z.string().min(1).optional(),
4050
+ limit: z.number().int().positive().max(MAX_LIMIT).optional(),
4051
+ maxChars: z.number().int().positive().optional()
4052
+ };
4053
+ var curateInput = {
4054
+ sessionId: z.string().min(1).optional(),
4055
+ limit: z.number().int().positive().max(MAX_LIMIT).optional()
4056
+ };
3216
4057
  var emptyInput = {};
3217
4058
  function registerTools(server, source, hooks = {}) {
3218
4059
  const ready = typeof source === "function" ? source : () => Promise.resolve(source);
@@ -3235,7 +4076,7 @@ function registerTools(server, source, hooks = {}) {
3235
4076
  const { backend, workspaceRoot, projectId, projectName } = deps;
3236
4077
  const mirrorToFiles = !deps.rootIsHomeOrFsRoot;
3237
4078
  const limit = clampLimit(args.limit, DEFAULT_READ_LIMIT);
3238
- const synced = mirrorToFiles ? await syncProjectFiles(backend, { projectId, workspaceRoot }) : 0;
4079
+ const sync = mirrorToFiles ? await syncProjectFiles(backend, { projectId, workspaceRoot }) : { synced: 0, pruned: 0, skipped: [] };
3239
4080
  const records = await backend.read({
3240
4081
  chain: defaultScopeChain(projectId),
3241
4082
  types: args.types,
@@ -3244,7 +4085,9 @@ function registerTools(server, source, hooks = {}) {
3244
4085
  return textResult({
3245
4086
  projectId,
3246
4087
  projectName,
3247
- projectFilesSynced: synced,
4088
+ projectFilesSynced: sync.synced,
4089
+ projectFilesPruned: sync.pruned,
4090
+ ...sync.skipped.length > 0 ? { skippedFiles: sync.skipped } : {},
3248
4091
  woven: weave(records),
3249
4092
  records: records.map(recordToJson)
3250
4093
  });
@@ -3274,10 +4117,18 @@ ${args.value}`);
3274
4117
  source: "user"
3275
4118
  });
3276
4119
  let projectFile = null;
3277
- if (mirrorToFiles && scope === "project" && args.type === "semantic") {
3278
- projectFile = await writeProjectFile(workspaceRoot, args.key, args.value);
4120
+ const shareable = scope === "project" && (args.type === "semantic" || args.type === "procedural");
4121
+ if (mirrorToFiles && shareable && args.share === true) {
4122
+ projectFile = await writeMemoryFile(workspaceRoot, {
4123
+ key: args.key,
4124
+ type: args.type,
4125
+ value: args.value,
4126
+ ...args.tags !== void 0 ? { tags: args.tags } : {},
4127
+ ...args.confidence !== void 0 ? { confidence: args.confidence } : {},
4128
+ source: "shared:user"
4129
+ });
3279
4130
  }
3280
- return textResult({ record: recordToJson(record), projectFile });
4131
+ return textResult({ record: recordToJson(record), projectFile, shared: projectFile !== null });
3281
4132
  });
3282
4133
  reg("capture", {
3283
4134
  title: "Consolidate session learnings",
@@ -3349,24 +4200,31 @@ ${f.value}`);
3349
4200
  inputSchema: searchInput
3350
4201
  }, async (args) => {
3351
4202
  const deps = await ready();
3352
- const { backend, workspaceRoot, projectId } = deps;
4203
+ const { backend, recorder, workspaceRoot, projectId } = deps;
3353
4204
  const mirrorToFiles = !deps.rootIsHomeOrFsRoot;
3354
4205
  const limit = clampLimit(args.limit, 20);
3355
- if (mirrorToFiles)
3356
- await syncProjectFiles(backend, { projectId, workspaceRoot });
3357
- const hits = await backend.search({
3358
- chain: defaultScopeChain(projectId),
3359
- query: args.query,
3360
- types: args.types,
3361
- limit
3362
- });
4206
+ const sources = args.sources ?? ["memories"];
4207
+ let hits = [];
4208
+ if (sources.includes("memories")) {
4209
+ if (mirrorToFiles)
4210
+ await syncProjectFiles(backend, { projectId, workspaceRoot });
4211
+ hits = await backend.search({
4212
+ chain: defaultScopeChain(projectId),
4213
+ query: args.query,
4214
+ types: args.types,
4215
+ limit
4216
+ });
4217
+ }
4218
+ let sessionHits = [];
4219
+ if (sources.includes("sessions")) {
4220
+ sessionHits = await recorder.searchSessions(projectId, args.query, limit);
4221
+ }
3363
4222
  return textResult({
3364
4223
  projectId,
3365
4224
  query: args.query,
3366
- hits: hits.map((h) => ({
3367
- snippet: h.snippet,
3368
- record: recordToJson(h.record)
3369
- }))
4225
+ sources,
4226
+ hits: hits.map((h) => ({ snippet: h.snippet, record: recordToJson(h.record) })),
4227
+ sessions: sessionHits
3370
4228
  });
3371
4229
  });
3372
4230
  reg("list", {
@@ -3407,7 +4265,7 @@ ${f.value}`);
3407
4265
  key: args.key
3408
4266
  });
3409
4267
  let fileRemoved = false;
3410
- if (mirrorToFiles && scope === "project" && args.type === "semantic") {
4268
+ if (mirrorToFiles && scope === "project" && (args.type === "semantic" || args.type === "procedural")) {
3411
4269
  fileRemoved = await deleteProjectFile(workspaceRoot, args.key);
3412
4270
  }
3413
4271
  return textResult({ removed, fileRemoved });
@@ -3434,6 +4292,82 @@ ${f.value}`);
3434
4292
  }
3435
4293
  return textResult({ workspaceRoot, results });
3436
4294
  });
4295
+ reg("recorder_setup", {
4296
+ title: "Install the Session Recorder (opt-in)",
4297
+ description: "Install (or, with `uninstall: true`, remove) the Prometheus Session Recorder \u2014 Claude Code hooks that capture your coding sessions LOCALLY so memory-mcp can recall what you did and distil durable knowledge. STRICTLY OPT-IN and reversible: this writes a hook script + 5 hook entries (SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd) into settings.json (backed up first). It records BEHAVIOUR, not code \u2014 tool names + short previews, with secrets redacted and sensitive-file contents skipped \u2014 as append-only JSONL under ~/.prometheus/recorder/. NOTHING is uploaded; it never leaves your machine. `scope`: 'project-local' (default \u2014 this project only, .claude/settings.local.json), 'project' (committed .claude/settings.json), or 'user' (~/.claude/settings.json, every project). After install/uninstall, RELOAD your Claude Code window(s). Claude-Code-specific (Cursor/VS Code do not run hooks).",
4298
+ inputSchema: recorderSetupInput
4299
+ }, async (args) => {
4300
+ const deps = await ready();
4301
+ try {
4302
+ const result = applyRecorderHooks({
4303
+ scope: args.scope ?? "project-local",
4304
+ projectRoot: deps.workspaceRoot,
4305
+ uninstall: args.uninstall === true
4306
+ });
4307
+ return textResult({ ok: true, ...result });
4308
+ } catch (err) {
4309
+ return textResult({
4310
+ ok: false,
4311
+ reason: `recorder_setup failed: ${err instanceof Error ? err.message : String(err)}`
4312
+ });
4313
+ }
4314
+ });
4315
+ reg("sessions", {
4316
+ title: "Browse recorded coding sessions",
4317
+ description: "Browse the Session Recorder's captured sessions for this project (requires recorder_setup). `{mode:'list'}` (default) \u2192 the most recent sessions with their header + stats (tool count, tools used, files touched) + M2 summary. `{mode:'get', sessionId}` \u2192 one session's condensed event log (user/assistant messages in full, tool calls as one-liners), capped by `maxChars`. Answers 'what did I do this week?' and 'have we tried X before?' \u2014 entirely from the LOCAL recorder DB.",
4318
+ inputSchema: sessionsInput
4319
+ }, async (args) => {
4320
+ const { recorder, projectId } = await ready();
4321
+ const mode = args.mode ?? "list";
4322
+ if (mode === "get") {
4323
+ const sessionId = (args.sessionId ?? "").trim();
4324
+ if (sessionId === "") {
4325
+ return textResult({ ok: false, reason: "mode 'get' requires a sessionId." });
4326
+ }
4327
+ const detail = await recorder.getSession(projectId, sessionId, args.maxChars ?? void 0);
4328
+ if (detail === null) {
4329
+ return textResult({ ok: false, reason: `no recorded session "${sessionId}" for this project.` });
4330
+ }
4331
+ return textResult({ ok: true, ...detail });
4332
+ }
4333
+ const limit = clampLimit(args.limit, 20);
4334
+ const sessions = await recorder.listSessions(projectId, limit);
4335
+ return textResult({ ok: true, projectId, count: sessions.length, sessions });
4336
+ });
4337
+ reg("curate", {
4338
+ title: "Distil recorded sessions into durable memory",
4339
+ description: "Curate recorded coding sessions (M2): distil a session's event log into a short summary + durable fact/procedure candidates via the configured extraction LLM (`PROMETHEUS_MEMORY_EXTRACT_PROVIDER`). Accepted project/semantic candidates land in `.prometheus/memories/` (git-versioned, PR-reviewable \u2014 the quality gate). `{sessionId}` curates that one session; with no args it curates the ended-but-not-yet-curated sessions (up to `limit`). Runs automatically after a session ends too. Requires an extractor \u2014 without one it is inactive (the recorder still works via `sessions`/`search`).",
4340
+ inputSchema: curateInput
4341
+ }, async (args) => {
4342
+ const deps = await ready();
4343
+ const { extractor, recorder, backend, projectId, workspaceRoot } = deps;
4344
+ if (extractor === null) {
4345
+ return textResult({
4346
+ ok: false,
4347
+ reason: "no extractor configured \u2014 set PROMETHEUS_MEMORY_EXTRACT_PROVIDER (mistral|openai|generic) + its key to enable curation. The recorder still works via sessions/search."
4348
+ });
4349
+ }
4350
+ const curateDeps = {
4351
+ backend,
4352
+ recorder,
4353
+ extractor,
4354
+ projectId,
4355
+ workspaceRoot,
4356
+ mirrorToFiles: !deps.rootIsHomeOrFsRoot
4357
+ };
4358
+ const sessionId = (args.sessionId ?? "").trim();
4359
+ if (sessionId !== "") {
4360
+ const outcome = await curateSession(curateDeps, sessionId);
4361
+ return textResult({ ok: outcome.curated, ...outcome });
4362
+ }
4363
+ const limit = clampLimit(args.limit, 10);
4364
+ const pending = await recorder.listUncuratedSessions(projectId, limit);
4365
+ const results = [];
4366
+ for (const s of pending) {
4367
+ results.push({ sessionId: s.sessionId, ...await curateSession(curateDeps, s.sessionId) });
4368
+ }
4369
+ return textResult({ ok: true, curatedCount: results.filter((r) => r.curated).length, results });
4370
+ });
3437
4371
  reg("status", {
3438
4372
  title: "Memory status / health check",
3439
4373
  description: "Health check for this project's agent memory. Reports the resolved workspace root, project id, DB path, how many records are stored (total + by scope), the embedding provider with a zero-cost key-reachability probe, and which quality levers are active (rerank / rewrite / temporal). CALL THIS to confirm where memory is stored, how much is there, and whether the API key works.",
@@ -3455,7 +4389,36 @@ ${f.value}`);
3455
4389
  embeddingsError = err instanceof Error ? err.message : String(err);
3456
4390
  }
3457
4391
  }
3458
- const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.11.4", { isDevBuild: false });
4392
+ const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.14.0", { isDevBuild: false });
4393
+ let recorder;
4394
+ try {
4395
+ const scopes = ["project-local", "project", "user"];
4396
+ let rec = recorderStatus({ scope: "project-local", projectRoot: workspaceRoot });
4397
+ for (const s of scopes) {
4398
+ const st = recorderStatus({ scope: s, projectRoot: workspaceRoot });
4399
+ if (st.installed) {
4400
+ rec = st;
4401
+ break;
4402
+ }
4403
+ }
4404
+ const totals = await deps.recorder.recorderTotals(projectId);
4405
+ recorder = {
4406
+ installed: rec.installed,
4407
+ scope: rec.scope,
4408
+ events: rec.events,
4409
+ settingsPath: rec.settingsPath,
4410
+ hookScriptPresent: rec.hookScriptPresent,
4411
+ sessions: totals.sessions,
4412
+ totalEvents: totals.events,
4413
+ retention: {
4414
+ days: Number(process.env.PROMETHEUS_RECORDER_RETENTION_DAYS ?? 90),
4415
+ maxSessions: Number(process.env.PROMETHEUS_RECORDER_MAX_SESSIONS ?? 300)
4416
+ },
4417
+ note: rec.installed ? "Session recorder is active (opt-in). It records LOCALLY only." : "Session recorder is OFF. Install with recorder_setup (strictly opt-in; records locally, nothing uploaded)."
4418
+ };
4419
+ } catch {
4420
+ recorder = { installed: false, error: "recorder status unavailable" };
4421
+ }
3459
4422
  const summary = deps.rootIsHomeOrFsRoot ? `Memory at ${dbPath}: ${stats.total} records, but the workspace resolved to ${workspaceRoot} (home/root) \u2014 open a project folder so memories scope and mirror correctly.` : `Memory at ${dbPath}: ${stats.total} records for project "${projectName}".${update.updateAvailable === true ? ` Update available: ${update.current} \u2192 ${update.latest} (ask me to run update via the context server's update_servers tool).` : ""}`;
3460
4423
  return textResult({
3461
4424
  installed: true,
@@ -3468,6 +4431,22 @@ ${f.value}`);
3468
4431
  autoSetup: deps.autoSetup
3469
4432
  },
3470
4433
  storage: { dbPath, projectFileMirror: mirrorToFiles },
4434
+ // M3-git L1 boot-check: is `.prometheus/memories/` committed (shared) or
4435
+ // gitignored (private/off)? Best-effort; never throws.
4436
+ sharedMemory: (() => {
4437
+ if (deps.rootIsHomeOrFsRoot)
4438
+ return { shared: false, note: "no project open" };
4439
+ let shared = false;
4440
+ try {
4441
+ shared = memoriesShared(workspaceRoot);
4442
+ } catch {
4443
+ shared = false;
4444
+ }
4445
+ return {
4446
+ shared,
4447
+ note: shared ? "Team memory sharing is ON: .prometheus/memories/ is committed to git; curated + share:true records travel with the repo." : "Team memory sharing is OFF: .prometheus/memories/ is gitignored (local memory still works fully). To share, add `!.prometheus/memories/` after `.prometheus/*` in .gitignore and commit the folder."
4448
+ };
4449
+ })(),
3471
4450
  records: { total: stats.total, byScope: stats.byScope },
3472
4451
  embeddings: {
3473
4452
  enabled: deps.embeddingsEnabled,
@@ -3482,6 +4461,7 @@ ${f.value}`);
3482
4461
  dedup: deps.dedupEnabled,
3483
4462
  extract: deps.extractorId
3484
4463
  },
4464
+ recorder,
3485
4465
  update,
3486
4466
  summary
3487
4467
  });
@@ -3491,7 +4471,7 @@ ${f.value}`);
3491
4471
  // dist/server.js
3492
4472
  var SERVER_IDENTITY = {
3493
4473
  name: "prometheus-memory-mcp",
3494
- version: "0.11.4",
4474
+ version: "0.14.0",
3495
4475
  title: "prom.codes Memory"
3496
4476
  };
3497
4477
  var SERVER_INSTRUCTIONS = "Persistent agent memory for this workspace \u2014 USE IT PROACTIVELY; the user will not tell you to. Protocol:\n1. ONE-TIME: if this workspace has no Prometheus memory rule yet, call memory_setup now (idempotent) so the protocol is installed into the runtime rule files and survives future sessions. (The server also auto-installs it on startup when a project rule file already exists \u2014 memory_setup covers the rest.)\n2. SESSION START: before any non-trivial task, call memory_read to recall facts, decisions and procedures from earlier sessions.\n3. DURING WORK: when the user states a durable preference, decision, correction or project fact, store it with memory_write (semantic for facts, procedural for how-tos) \u2014 without being asked.\n4. LOOK-UP: use memory_search for keyword recall when memory_read is not specific enough.\n5. SESSION END: consolidate what was learned with memory_capture.\nCall memory_status anytime to check what is stored and whether the rule is installed. Never store secrets, API keys or credentials \u2014 such writes are rejected.";
@@ -3526,6 +4506,8 @@ async function main() {
3526
4506
  onToolCall: (tool) => heartbeat.update({ lastTool: tool, lastToolCallAt: Date.now() })
3527
4507
  });
3528
4508
  let watchdog = null;
4509
+ let recorderTimer = null;
4510
+ const RECORDER_INGEST_MS = 6e4;
3529
4511
  let shuttingDown = false;
3530
4512
  const shutdown = async (reason) => {
3531
4513
  if (shuttingDown)
@@ -3534,6 +4516,8 @@ async function main() {
3534
4516
  process.stderr.write(`prometheus-memory-mcp: ${reason}, shutting down
3535
4517
  `);
3536
4518
  watchdog?.stop();
4519
+ if (recorderTimer !== null)
4520
+ clearInterval(recorderTimer);
3537
4521
  heartbeat.stop();
3538
4522
  try {
3539
4523
  await server.close();
@@ -3590,6 +4574,38 @@ async function main() {
3590
4574
  });
3591
4575
  }
3592
4576
  composedResolve(composed);
4577
+ const ingestOnce = async () => {
4578
+ try {
4579
+ const r = await composed.recorder.ingestSpoolDir(composed.projectId, env);
4580
+ if (r.events > 0) {
4581
+ process.stderr.write(`prometheus-memory-mcp: recorder ingested ${r.events} event(s) from ${r.sessions} session(s); reclaimed ${r.deletedSpools} spool(s)
4582
+ `);
4583
+ }
4584
+ const c = composed;
4585
+ if (c.extractor !== null) {
4586
+ const pending = await c.recorder.listUncuratedSessions(c.projectId, 3);
4587
+ for (const s of pending) {
4588
+ const outcome = await curateSession({
4589
+ backend: c.backend,
4590
+ recorder: c.recorder,
4591
+ extractor: c.extractor,
4592
+ projectId: c.projectId,
4593
+ workspaceRoot: c.workspaceRoot,
4594
+ mirrorToFiles: !c.rootIsHomeOrFsRoot
4595
+ }, s.sessionId);
4596
+ if (outcome.curated) {
4597
+ process.stderr.write(`prometheus-memory-mcp: curated session ${s.sessionId} (${outcome.facts ?? 0} facts, ${outcome.procedures ?? 0} procedures)
4598
+ `);
4599
+ }
4600
+ }
4601
+ }
4602
+ await composed.recorder.applyRecorderRetention(env);
4603
+ } catch {
4604
+ }
4605
+ };
4606
+ void ingestOnce();
4607
+ recorderTimer = setInterval(() => void ingestOnce(), RECORDER_INGEST_MS);
4608
+ recorderTimer.unref?.();
3593
4609
  };
3594
4610
  if (eagerVia !== null) {
3595
4611
  boot(void 0, eagerVia);