@prom.codes/memory-mcp 0.11.3 → 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.
- package/dist/bin.js +1192 -144
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -34,11 +34,43 @@ var LANGUAGE_IDS = [
|
|
|
34
34
|
];
|
|
35
35
|
|
|
36
36
|
// ../shared/dist/update-check.js
|
|
37
|
+
import { exec } from "node:child_process";
|
|
37
38
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
38
39
|
import { homedir } from "node:os";
|
|
39
40
|
import { join } from "node:path";
|
|
40
41
|
import { fileURLToPath } from "node:url";
|
|
41
|
-
var
|
|
42
|
+
var UPGRADE_BASE = "npm install -g @prom.codes/context-mcp @prom.codes/memory-mcp @prom.codes/saver";
|
|
43
|
+
var NATIVE_BUILD_PACKAGES = "better-sqlite3,tree-sitter";
|
|
44
|
+
function upgradeCommandFor(npmMajor) {
|
|
45
|
+
if (npmMajor !== null && npmMajor >= 12) {
|
|
46
|
+
return `${UPGRADE_BASE} --allow-scripts=${NATIVE_BUILD_PACKAGES}`;
|
|
47
|
+
}
|
|
48
|
+
return `${UPGRADE_BASE} --ignore-scripts=false --foreground-scripts`;
|
|
49
|
+
}
|
|
50
|
+
var UPGRADE_COMMAND = upgradeCommandFor(null);
|
|
51
|
+
var npmMajorPromise;
|
|
52
|
+
function detectNpmMajor(execImpl = exec) {
|
|
53
|
+
if (npmMajorPromise === void 0) {
|
|
54
|
+
npmMajorPromise = new Promise((resolvePromise) => {
|
|
55
|
+
try {
|
|
56
|
+
execImpl("npm --version", { timeout: 4e3, windowsHide: true }, (err, stdout) => {
|
|
57
|
+
if (err) {
|
|
58
|
+
resolvePromise(null);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const m = /(\d+)\./.exec(String(stdout).trim());
|
|
62
|
+
resolvePromise(m ? Number(m[1]) : null);
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
resolvePromise(null);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return npmMajorPromise;
|
|
70
|
+
}
|
|
71
|
+
async function resolveUpgradeCommand(execImpl = exec) {
|
|
72
|
+
return upgradeCommandFor(await detectNpmMajor(execImpl));
|
|
73
|
+
}
|
|
42
74
|
async function packageIdentity(binImportMetaUrl) {
|
|
43
75
|
try {
|
|
44
76
|
const binPath = fileURLToPath(binImportMetaUrl);
|
|
@@ -108,7 +140,7 @@ async function syncAvailabilityMarker(dir, name, current, latest, updateAvailabl
|
|
|
108
140
|
name,
|
|
109
141
|
current,
|
|
110
142
|
latest,
|
|
111
|
-
command:
|
|
143
|
+
command: await resolveUpgradeCommand(),
|
|
112
144
|
notedAt: Date.now()
|
|
113
145
|
};
|
|
114
146
|
await mkdir(dir, { recursive: true }).catch(() => void 0);
|
|
@@ -231,7 +263,7 @@ function notify(log, name, current, latest) {
|
|
|
231
263
|
|
|
232
264
|
// ../shared/dist/update-info.js
|
|
233
265
|
async function buildUpdateStatus(pkgName, currentVersion, options = {}) {
|
|
234
|
-
const base = { current: currentVersion, command:
|
|
266
|
+
const base = { current: currentVersion, command: await resolveUpgradeCommand() };
|
|
235
267
|
if (options.isDevBuild === true) {
|
|
236
268
|
return {
|
|
237
269
|
...base,
|
|
@@ -261,7 +293,7 @@ async function buildUpdateStatus(pkgName, currentVersion, options = {}) {
|
|
|
261
293
|
|
|
262
294
|
// ../shared/dist/workspace-root.js
|
|
263
295
|
import { homedir as homedir2 } from "node:os";
|
|
264
|
-
import { dirname, resolve } from "node:path";
|
|
296
|
+
import { dirname, join as join2, resolve } from "node:path";
|
|
265
297
|
function isHomeOrFilesystemRoot(root) {
|
|
266
298
|
const abs = resolve(root);
|
|
267
299
|
if (abs === "")
|
|
@@ -276,18 +308,18 @@ function isHomeOrFilesystemRoot(root) {
|
|
|
276
308
|
// ../shared/dist/heartbeat.js
|
|
277
309
|
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
278
310
|
import { homedir as homedir3 } from "node:os";
|
|
279
|
-
import { join as
|
|
311
|
+
import { join as join3 } from "node:path";
|
|
280
312
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
|
|
281
313
|
var STALE_AFTER_MS = 5 * 6e4;
|
|
282
314
|
function defaultStatusDir(env = process.env) {
|
|
283
315
|
const override = (env.PROMETHEUS_STATUS_DIR ?? "").trim();
|
|
284
316
|
if (override !== "")
|
|
285
317
|
return override;
|
|
286
|
-
return
|
|
318
|
+
return join3(homedir3(), ".prometheus", "status");
|
|
287
319
|
}
|
|
288
320
|
function startHeartbeat(options) {
|
|
289
321
|
const dir = options.dir ?? defaultStatusDir(options.env ?? process.env);
|
|
290
|
-
const file =
|
|
322
|
+
const file = join3(dir, `${options.server}-${process.pid}.json`);
|
|
291
323
|
const intervalMs = options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
292
324
|
let record = {
|
|
293
325
|
server: options.server,
|
|
@@ -396,8 +428,8 @@ function createIdleWatchdog(options) {
|
|
|
396
428
|
|
|
397
429
|
// dist/composition.js
|
|
398
430
|
import { createHash } from "node:crypto";
|
|
399
|
-
import { homedir as
|
|
400
|
-
import { basename, join as
|
|
431
|
+
import { homedir as homedir5 } from "node:os";
|
|
432
|
+
import { basename, join as join6, resolve as resolve2 } from "node:path";
|
|
401
433
|
|
|
402
434
|
// ../embeddings-openai-compat/dist/index.js
|
|
403
435
|
var DEFAULT_BATCH = 96;
|
|
@@ -1516,6 +1548,26 @@ function requireApiKey(env) {
|
|
|
1516
1548
|
|
|
1517
1549
|
// dist/extraction.js
|
|
1518
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
|
+
}
|
|
1519
1571
|
function parseExtraction(raw, maxFacts = 12, maxValueChars = 2e3) {
|
|
1520
1572
|
const match = raw.match(/\[[\s\S]*\]/);
|
|
1521
1573
|
if (!match)
|
|
@@ -1575,47 +1627,60 @@ var OpenAICompatExtractor = class {
|
|
|
1575
1627
|
const trimmed = text.trim();
|
|
1576
1628
|
if (trimmed === "")
|
|
1577
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) {
|
|
1578
1651
|
const body = JSON.stringify({
|
|
1579
1652
|
model: this.model,
|
|
1580
1653
|
temperature: this.#temperature,
|
|
1581
1654
|
messages: [
|
|
1582
|
-
{ role: "system", content:
|
|
1583
|
-
{ role: "user", content:
|
|
1584
|
-
|
|
1585
|
-
${trimmed}` }
|
|
1655
|
+
{ role: "system", content: system },
|
|
1656
|
+
{ role: "user", content: user }
|
|
1586
1657
|
]
|
|
1587
1658
|
});
|
|
1588
1659
|
const headers = { "content-type": "application/json" };
|
|
1589
1660
|
if (this.#apiKey !== void 0 && this.#apiKey !== "") {
|
|
1590
1661
|
headers.authorization = `Bearer ${this.#apiKey}`;
|
|
1591
1662
|
}
|
|
1592
|
-
let lastErr;
|
|
1593
1663
|
for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
|
|
1594
1664
|
try {
|
|
1595
1665
|
const res = await this.#fetch(this.#url, {
|
|
1596
1666
|
method: "POST",
|
|
1597
1667
|
headers,
|
|
1598
1668
|
body,
|
|
1599
|
-
...
|
|
1669
|
+
...signal ? { signal } : {}
|
|
1600
1670
|
});
|
|
1601
1671
|
if (res.status === 429 || res.status >= 500) {
|
|
1602
|
-
lastErr = new Error(`extractor HTTP ${res.status}`);
|
|
1603
1672
|
} else if (!res.ok) {
|
|
1604
|
-
return
|
|
1673
|
+
return null;
|
|
1605
1674
|
} else {
|
|
1606
1675
|
const json = await res.json();
|
|
1607
|
-
|
|
1608
|
-
return parseExtraction(content);
|
|
1676
|
+
return json.choices?.[0]?.message?.content ?? "";
|
|
1609
1677
|
}
|
|
1610
|
-
} catch
|
|
1611
|
-
lastErr = err;
|
|
1678
|
+
} catch {
|
|
1612
1679
|
}
|
|
1613
|
-
if (attempt < this.#maxRetries)
|
|
1680
|
+
if (attempt < this.#maxRetries)
|
|
1614
1681
|
await delay(this.#retryBaseMs * 2 ** attempt);
|
|
1615
|
-
}
|
|
1616
1682
|
}
|
|
1617
|
-
|
|
1618
|
-
return [];
|
|
1683
|
+
return null;
|
|
1619
1684
|
}
|
|
1620
1685
|
};
|
|
1621
1686
|
function delay(ms) {
|
|
@@ -1697,10 +1762,347 @@ var OpenAICompatRewriter = class {
|
|
|
1697
1762
|
|
|
1698
1763
|
// dist/sqlite.js
|
|
1699
1764
|
import { randomUUID } from "node:crypto";
|
|
1700
|
-
import { mkdirSync as
|
|
1701
|
-
import { dirname as
|
|
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";
|
|
1702
1767
|
import Database from "better-sqlite3";
|
|
1703
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
|
+
|
|
1704
2106
|
// dist/rrf.js
|
|
1705
2107
|
function reciprocalRankFusion(lists, options = {}) {
|
|
1706
2108
|
const k = options.k ?? 60;
|
|
@@ -1961,6 +2363,54 @@ CREATE TRIGGER IF NOT EXISTS agent_memory_vec_ad AFTER DELETE ON agent_memory BE
|
|
|
1961
2363
|
DELETE FROM agent_memory_vec WHERE record_id = old.id;
|
|
1962
2364
|
END;
|
|
1963
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;
|
|
1964
2414
|
function vectorToBlob(vector) {
|
|
1965
2415
|
return Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
1966
2416
|
}
|
|
@@ -2011,6 +2461,70 @@ function rowToRecord(row) {
|
|
|
2011
2461
|
updatedAt: row.updated_at
|
|
2012
2462
|
};
|
|
2013
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
|
+
}
|
|
2014
2528
|
var SqliteMemoryBackend = class {
|
|
2015
2529
|
db;
|
|
2016
2530
|
embedder;
|
|
@@ -2025,7 +2539,7 @@ var SqliteMemoryBackend = class {
|
|
|
2025
2539
|
closed = false;
|
|
2026
2540
|
constructor(dbPath, opts = {}) {
|
|
2027
2541
|
if (dbPath !== ":memory:") {
|
|
2028
|
-
|
|
2542
|
+
mkdirSync3(dirname3(dbPath), { recursive: true });
|
|
2029
2543
|
}
|
|
2030
2544
|
this.db = new Database(dbPath);
|
|
2031
2545
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -2033,6 +2547,8 @@ var SqliteMemoryBackend = class {
|
|
|
2033
2547
|
this.db.exec(SCHEMA);
|
|
2034
2548
|
this.db.exec(FTS_SCHEMA);
|
|
2035
2549
|
this.db.exec(VEC_SCHEMA);
|
|
2550
|
+
this.db.exec(RECORDER_SCHEMA);
|
|
2551
|
+
this.db.exec(RECORDER_FTS_SCHEMA);
|
|
2036
2552
|
this.db.exec(`INSERT INTO agent_memory_fts (agent_memory_fts) VALUES ('rebuild')`);
|
|
2037
2553
|
this.embedder = opts.embedder;
|
|
2038
2554
|
this.reranker = opts.reranker;
|
|
@@ -2460,6 +2976,183 @@ ${h.record.value}`
|
|
|
2460
2976
|
this.audit("consolidate", { scope: input.scope, scopeId: input.scopeId }, `records=${written.length}`);
|
|
2461
2977
|
return written;
|
|
2462
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
|
+
}
|
|
2463
3156
|
async close() {
|
|
2464
3157
|
if (this.closed)
|
|
2465
3158
|
return;
|
|
@@ -2478,7 +3171,7 @@ function projectIdFor(workspaceRoot) {
|
|
|
2478
3171
|
return createHash("sha256").update(abs).digest("hex").slice(0, 16);
|
|
2479
3172
|
}
|
|
2480
3173
|
function defaultMemoryDbPath() {
|
|
2481
|
-
return
|
|
3174
|
+
return join6(homedir5(), ".prometheus", "memory.db");
|
|
2482
3175
|
}
|
|
2483
3176
|
function intEnv(env, name, def) {
|
|
2484
3177
|
const raw = env[name];
|
|
@@ -2746,6 +3439,7 @@ function composeFromEnv(opts) {
|
|
|
2746
3439
|
});
|
|
2747
3440
|
return {
|
|
2748
3441
|
backend,
|
|
3442
|
+
recorder: backend,
|
|
2749
3443
|
workspaceRoot,
|
|
2750
3444
|
projectId,
|
|
2751
3445
|
projectName,
|
|
@@ -2767,47 +3461,57 @@ function composeFromEnv(opts) {
|
|
|
2767
3461
|
};
|
|
2768
3462
|
}
|
|
2769
3463
|
|
|
2770
|
-
// dist/roots.js
|
|
2771
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2772
|
-
async function rootFromClient(server, timeoutMs = 2500) {
|
|
2773
|
-
let supportsRoots = false;
|
|
2774
|
-
try {
|
|
2775
|
-
supportsRoots = server.getClientCapabilities()?.roots != null;
|
|
2776
|
-
} catch {
|
|
2777
|
-
return null;
|
|
2778
|
-
}
|
|
2779
|
-
if (!supportsRoots)
|
|
2780
|
-
return null;
|
|
2781
|
-
let res;
|
|
2782
|
-
try {
|
|
2783
|
-
res = await server.listRoots(void 0, { timeout: timeoutMs });
|
|
2784
|
-
} catch {
|
|
2785
|
-
return null;
|
|
2786
|
-
}
|
|
2787
|
-
const roots = res?.roots ?? [];
|
|
2788
|
-
for (const r of roots) {
|
|
2789
|
-
const uri = typeof r?.uri === "string" ? r.uri : "";
|
|
2790
|
-
if (uri.startsWith("file://")) {
|
|
2791
|
-
try {
|
|
2792
|
-
return fileURLToPath2(uri);
|
|
2793
|
-
} catch {
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
return null;
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
// dist/server.js
|
|
2801
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2802
|
-
|
|
2803
|
-
// dist/tools.js
|
|
2804
|
-
import { z } from "zod";
|
|
2805
|
-
|
|
2806
3464
|
// dist/project-files.js
|
|
3465
|
+
import { execFileSync } from "node:child_process";
|
|
2807
3466
|
import * as fs from "node:fs/promises";
|
|
2808
3467
|
import * as path from "node:path";
|
|
2809
3468
|
var MEMORIES_DIR = path.join(".prometheus", "memories");
|
|
2810
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
|
+
}
|
|
2811
3515
|
function memoriesDir(workspaceRoot) {
|
|
2812
3516
|
return path.join(workspaceRoot, MEMORIES_DIR);
|
|
2813
3517
|
}
|
|
@@ -2821,13 +3525,25 @@ function keyToFilename(key) {
|
|
|
2821
3525
|
function filenameToKey(filename) {
|
|
2822
3526
|
return filename.replace(/\.md$/i, "");
|
|
2823
3527
|
}
|
|
2824
|
-
async function
|
|
3528
|
+
async function writeMemoryFile(workspaceRoot, f) {
|
|
2825
3529
|
const dir = memoriesDir(workspaceRoot);
|
|
2826
3530
|
await fs.mkdir(dir, { recursive: true });
|
|
2827
|
-
const file = path.join(dir, keyToFilename(key));
|
|
2828
|
-
await fs.writeFile(file,
|
|
3531
|
+
const file = path.join(dir, keyToFilename(f.key));
|
|
3532
|
+
await fs.writeFile(file, serializeMemoryFile(f), "utf-8");
|
|
2829
3533
|
return file;
|
|
2830
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
|
+
}
|
|
2831
3547
|
async function deleteProjectFile(workspaceRoot, key) {
|
|
2832
3548
|
const file = path.join(memoriesDir(workspaceRoot), keyToFilename(key));
|
|
2833
3549
|
try {
|
|
@@ -2861,67 +3577,196 @@ async function listProjectFiles(workspaceRoot) {
|
|
|
2861
3577
|
async function syncProjectFiles(backend, input) {
|
|
2862
3578
|
const files = await listProjectFiles(input.workspaceRoot);
|
|
2863
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;
|
|
2864
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);
|
|
2865
3592
|
await backend.write({
|
|
2866
3593
|
projectId: input.projectId,
|
|
2867
3594
|
scope: "project",
|
|
2868
3595
|
scopeId,
|
|
2869
|
-
type:
|
|
3596
|
+
type: parsed.type,
|
|
2870
3597
|
key: file.key,
|
|
2871
|
-
value:
|
|
3598
|
+
value: parsed.body,
|
|
3599
|
+
...parsed.tags !== void 0 ? { tags: parsed.tags } : {},
|
|
3600
|
+
...parsed.confidence !== void 0 ? { confidence: parsed.confidence } : {},
|
|
2872
3601
|
source: PROJECT_FILE_SOURCE
|
|
2873
3602
|
});
|
|
3603
|
+
synced++;
|
|
3604
|
+
}
|
|
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++;
|
|
2874
3620
|
}
|
|
2875
|
-
return
|
|
3621
|
+
return { synced, pruned, skipped };
|
|
2876
3622
|
}
|
|
2877
3623
|
|
|
2878
|
-
// dist/
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
{ name: "sentry-token", regex: /\bsntrys_[A-Za-z0-9+/=_-]{20,}/ },
|
|
2889
|
-
{ name: "vercel-token", regex: /\bvc[kp]_[A-Za-z0-9]{20,}/ },
|
|
2890
|
-
{ name: "huggingface-token", regex: /\bhf_[A-Za-z0-9]{30,}/ },
|
|
2891
|
-
{ name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{30,}/ },
|
|
2892
|
-
{ name: "voyage-key", regex: /\bpa-[A-Za-z0-9_-]{30,}/ },
|
|
2893
|
-
{ name: "google-api-key", regex: /\bAIza[A-Za-z0-9_-]{30,}/ },
|
|
2894
|
-
{ name: "sovrgpt-key", regex: /\bsov_[a-f0-9]{40,}/ },
|
|
2895
|
-
{ name: "prometheus-key", regex: /\bprom_(?:live|test)_[A-Za-z0-9]{10,}/ },
|
|
2896
|
-
{ name: "aws-access-key", regex: /\bAKIA[A-Z0-9]{16}\b/ },
|
|
2897
|
-
{ name: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/ },
|
|
2898
|
-
{ name: "private-key-block", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
2899
|
-
{ name: "authorization-header", regex: /\bAuthorization:\s*(?:Bearer|Basic)\s+\S{8,}/i },
|
|
2900
|
-
{
|
|
2901
|
-
name: "connection-string-credentials",
|
|
2902
|
-
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);
|
|
2903
3634
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
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 };
|
|
2910
3646
|
}
|
|
2911
|
-
|
|
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 };
|
|
2912
3728
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
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;
|
|
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
|
+
}
|
|
2918
3756
|
}
|
|
3757
|
+
return null;
|
|
2919
3758
|
}
|
|
2920
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
|
+
|
|
2921
3766
|
// dist/setup.js
|
|
2922
|
-
import { existsSync, readFileSync as
|
|
3767
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
2923
3768
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
2924
|
-
import { dirname as
|
|
3769
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
2925
3770
|
var MEMORY_RUNTIMES = [
|
|
2926
3771
|
"claude-code",
|
|
2927
3772
|
"cursor",
|
|
@@ -2965,13 +3810,13 @@ alwaysApply: true
|
|
|
2965
3810
|
var TARGETS = {
|
|
2966
3811
|
"claude-code": { relPath: "CLAUDE.md", mode: "block", detect: "CLAUDE.md" },
|
|
2967
3812
|
cursor: {
|
|
2968
|
-
relPath:
|
|
3813
|
+
relPath: join8(".cursor", "rules", "prometheus-memory.mdc"),
|
|
2969
3814
|
mode: "file",
|
|
2970
3815
|
fileContent: CURSOR_FRONTMATTER + withMarkers(RULE_BLOCK) + "\n",
|
|
2971
3816
|
detect: ".cursor"
|
|
2972
3817
|
},
|
|
2973
3818
|
augment: {
|
|
2974
|
-
relPath:
|
|
3819
|
+
relPath: join8(".augment", "rules", "prometheus-memory.md"),
|
|
2975
3820
|
mode: "file",
|
|
2976
3821
|
fileContent: withMarkers(RULE_BLOCK) + "\n",
|
|
2977
3822
|
detect: ".augment"
|
|
@@ -2979,19 +3824,19 @@ var TARGETS = {
|
|
|
2979
3824
|
agents: { relPath: "AGENTS.md", mode: "block", detect: "AGENTS.md" }
|
|
2980
3825
|
};
|
|
2981
3826
|
function detectRuntimes(workspaceRoot) {
|
|
2982
|
-
const found = MEMORY_RUNTIMES.filter((rt) =>
|
|
3827
|
+
const found = MEMORY_RUNTIMES.filter((rt) => existsSync2(join8(workspaceRoot, TARGETS[rt].detect)));
|
|
2983
3828
|
return found.length > 0 ? found : ["agents"];
|
|
2984
3829
|
}
|
|
2985
3830
|
function existingRuntimes(workspaceRoot) {
|
|
2986
|
-
return MEMORY_RUNTIMES.filter((rt) =>
|
|
3831
|
+
return MEMORY_RUNTIMES.filter((rt) => existsSync2(join8(workspaceRoot, TARGETS[rt].detect)));
|
|
2987
3832
|
}
|
|
2988
3833
|
function installedRuntimes(workspaceRoot) {
|
|
2989
3834
|
return MEMORY_RUNTIMES.filter((rt) => {
|
|
2990
|
-
const p =
|
|
2991
|
-
if (!
|
|
3835
|
+
const p = join8(workspaceRoot, TARGETS[rt].relPath);
|
|
3836
|
+
if (!existsSync2(p))
|
|
2992
3837
|
return false;
|
|
2993
3838
|
try {
|
|
2994
|
-
return
|
|
3839
|
+
return readFileSync4(p, "utf-8").includes(BLOCK_START);
|
|
2995
3840
|
} catch {
|
|
2996
3841
|
return false;
|
|
2997
3842
|
}
|
|
@@ -3016,14 +3861,14 @@ function upsertBlock(existing, block) {
|
|
|
3016
3861
|
}
|
|
3017
3862
|
async function installRuntime(workspaceRoot, runtime) {
|
|
3018
3863
|
const target = TARGETS[runtime];
|
|
3019
|
-
const absPath =
|
|
3020
|
-
const exists =
|
|
3864
|
+
const absPath = join8(workspaceRoot, target.relPath);
|
|
3865
|
+
const exists = existsSync2(absPath);
|
|
3021
3866
|
const before = exists ? await readFile3(absPath, "utf-8") : "";
|
|
3022
3867
|
const after = target.mode === "file" ? target.fileContent : upsertBlock(before, RULE_BLOCK);
|
|
3023
3868
|
if (exists && before === after) {
|
|
3024
3869
|
return { runtime, path: absPath, action: "unchanged" };
|
|
3025
3870
|
}
|
|
3026
|
-
await mkdir3(
|
|
3871
|
+
await mkdir3(dirname4(absPath), { recursive: true });
|
|
3027
3872
|
await writeFile3(absPath, after, "utf-8");
|
|
3028
3873
|
return { runtime, path: absPath, action: exists ? "updated" : "created" };
|
|
3029
3874
|
}
|
|
@@ -3111,6 +3956,9 @@ function recordToJson(rec) {
|
|
|
3111
3956
|
value: rec.value,
|
|
3112
3957
|
confidence: rec.confidence ?? null,
|
|
3113
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",
|
|
3114
3962
|
tags: rec.tags ?? [],
|
|
3115
3963
|
useCount: rec.useCount,
|
|
3116
3964
|
createdAt: rec.createdAt,
|
|
@@ -3142,7 +3990,13 @@ var writeInput = {
|
|
|
3142
3990
|
key: z.string().min(1, "key must not be empty"),
|
|
3143
3991
|
value: z.string().min(1, "value must not be empty"),
|
|
3144
3992
|
confidence: z.number().min(0).max(1).optional(),
|
|
3145
|
-
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()
|
|
3146
4000
|
};
|
|
3147
4001
|
var captureInput = {
|
|
3148
4002
|
sessionId: z.string().min(1, "sessionId must not be empty"),
|
|
@@ -3175,12 +4029,31 @@ var deleteInput = {
|
|
|
3175
4029
|
var searchInput = {
|
|
3176
4030
|
query: z.string().min(1, "query must not be empty"),
|
|
3177
4031
|
types: z.array(typeEnum).min(1).optional(),
|
|
3178
|
-
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()
|
|
3179
4038
|
};
|
|
3180
4039
|
var runtimeEnum = z.enum(MEMORY_RUNTIMES);
|
|
3181
4040
|
var setupInput = {
|
|
3182
4041
|
runtimes: z.array(runtimeEnum).min(1).optional()
|
|
3183
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
|
+
};
|
|
3184
4057
|
var emptyInput = {};
|
|
3185
4058
|
function registerTools(server, source, hooks = {}) {
|
|
3186
4059
|
const ready = typeof source === "function" ? source : () => Promise.resolve(source);
|
|
@@ -3203,7 +4076,7 @@ function registerTools(server, source, hooks = {}) {
|
|
|
3203
4076
|
const { backend, workspaceRoot, projectId, projectName } = deps;
|
|
3204
4077
|
const mirrorToFiles = !deps.rootIsHomeOrFsRoot;
|
|
3205
4078
|
const limit = clampLimit(args.limit, DEFAULT_READ_LIMIT);
|
|
3206
|
-
const
|
|
4079
|
+
const sync = mirrorToFiles ? await syncProjectFiles(backend, { projectId, workspaceRoot }) : { synced: 0, pruned: 0, skipped: [] };
|
|
3207
4080
|
const records = await backend.read({
|
|
3208
4081
|
chain: defaultScopeChain(projectId),
|
|
3209
4082
|
types: args.types,
|
|
@@ -3212,7 +4085,9 @@ function registerTools(server, source, hooks = {}) {
|
|
|
3212
4085
|
return textResult({
|
|
3213
4086
|
projectId,
|
|
3214
4087
|
projectName,
|
|
3215
|
-
projectFilesSynced: synced,
|
|
4088
|
+
projectFilesSynced: sync.synced,
|
|
4089
|
+
projectFilesPruned: sync.pruned,
|
|
4090
|
+
...sync.skipped.length > 0 ? { skippedFiles: sync.skipped } : {},
|
|
3216
4091
|
woven: weave(records),
|
|
3217
4092
|
records: records.map(recordToJson)
|
|
3218
4093
|
});
|
|
@@ -3242,10 +4117,18 @@ ${args.value}`);
|
|
|
3242
4117
|
source: "user"
|
|
3243
4118
|
});
|
|
3244
4119
|
let projectFile = null;
|
|
3245
|
-
|
|
3246
|
-
|
|
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
|
+
});
|
|
3247
4130
|
}
|
|
3248
|
-
return textResult({ record: recordToJson(record), projectFile });
|
|
4131
|
+
return textResult({ record: recordToJson(record), projectFile, shared: projectFile !== null });
|
|
3249
4132
|
});
|
|
3250
4133
|
reg("capture", {
|
|
3251
4134
|
title: "Consolidate session learnings",
|
|
@@ -3317,24 +4200,31 @@ ${f.value}`);
|
|
|
3317
4200
|
inputSchema: searchInput
|
|
3318
4201
|
}, async (args) => {
|
|
3319
4202
|
const deps = await ready();
|
|
3320
|
-
const { backend, workspaceRoot, projectId } = deps;
|
|
4203
|
+
const { backend, recorder, workspaceRoot, projectId } = deps;
|
|
3321
4204
|
const mirrorToFiles = !deps.rootIsHomeOrFsRoot;
|
|
3322
4205
|
const limit = clampLimit(args.limit, 20);
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
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
|
+
}
|
|
3331
4222
|
return textResult({
|
|
3332
4223
|
projectId,
|
|
3333
4224
|
query: args.query,
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
}))
|
|
4225
|
+
sources,
|
|
4226
|
+
hits: hits.map((h) => ({ snippet: h.snippet, record: recordToJson(h.record) })),
|
|
4227
|
+
sessions: sessionHits
|
|
3338
4228
|
});
|
|
3339
4229
|
});
|
|
3340
4230
|
reg("list", {
|
|
@@ -3375,7 +4265,7 @@ ${f.value}`);
|
|
|
3375
4265
|
key: args.key
|
|
3376
4266
|
});
|
|
3377
4267
|
let fileRemoved = false;
|
|
3378
|
-
if (mirrorToFiles && scope === "project" && args.type === "semantic") {
|
|
4268
|
+
if (mirrorToFiles && scope === "project" && (args.type === "semantic" || args.type === "procedural")) {
|
|
3379
4269
|
fileRemoved = await deleteProjectFile(workspaceRoot, args.key);
|
|
3380
4270
|
}
|
|
3381
4271
|
return textResult({ removed, fileRemoved });
|
|
@@ -3402,6 +4292,82 @@ ${f.value}`);
|
|
|
3402
4292
|
}
|
|
3403
4293
|
return textResult({ workspaceRoot, results });
|
|
3404
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
|
+
});
|
|
3405
4371
|
reg("status", {
|
|
3406
4372
|
title: "Memory status / health check",
|
|
3407
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.",
|
|
@@ -3423,7 +4389,36 @@ ${f.value}`);
|
|
|
3423
4389
|
embeddingsError = err instanceof Error ? err.message : String(err);
|
|
3424
4390
|
}
|
|
3425
4391
|
}
|
|
3426
|
-
const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.
|
|
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
|
+
}
|
|
3427
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).` : ""}`;
|
|
3428
4423
|
return textResult({
|
|
3429
4424
|
installed: true,
|
|
@@ -3436,6 +4431,22 @@ ${f.value}`);
|
|
|
3436
4431
|
autoSetup: deps.autoSetup
|
|
3437
4432
|
},
|
|
3438
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
|
+
})(),
|
|
3439
4450
|
records: { total: stats.total, byScope: stats.byScope },
|
|
3440
4451
|
embeddings: {
|
|
3441
4452
|
enabled: deps.embeddingsEnabled,
|
|
@@ -3450,6 +4461,7 @@ ${f.value}`);
|
|
|
3450
4461
|
dedup: deps.dedupEnabled,
|
|
3451
4462
|
extract: deps.extractorId
|
|
3452
4463
|
},
|
|
4464
|
+
recorder,
|
|
3453
4465
|
update,
|
|
3454
4466
|
summary
|
|
3455
4467
|
});
|
|
@@ -3459,7 +4471,7 @@ ${f.value}`);
|
|
|
3459
4471
|
// dist/server.js
|
|
3460
4472
|
var SERVER_IDENTITY = {
|
|
3461
4473
|
name: "prometheus-memory-mcp",
|
|
3462
|
-
version: "0.
|
|
4474
|
+
version: "0.14.0",
|
|
3463
4475
|
title: "prom.codes Memory"
|
|
3464
4476
|
};
|
|
3465
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.";
|
|
@@ -3468,7 +4480,7 @@ var SERVER_INSTRUCTIONS = "Persistent agent memory for this workspace \u2014 USE
|
|
|
3468
4480
|
function looksLikeMissingNativeBinding(msg) {
|
|
3469
4481
|
return /bindings file|better_sqlite3\.node|could not locate the bindings|node_module_version|was compiled against a different|invalid elf|\.node['"\s]/i.test(msg);
|
|
3470
4482
|
}
|
|
3471
|
-
var NATIVE_BINDING_HINT = '\nThis looks like the native `better-sqlite3` module failed to load \u2014
|
|
4483
|
+
var NATIVE_BINDING_HINT = '\nThis looks like the native `better-sqlite3` module failed to load \u2014 the\ninstall script that builds it was skipped, so the binary was never produced.\nOn npm v12+ install scripts are opt-in by default; on older npm it\'s usually\n`ignore-scripts=true` hardening. Install globally, allowing the native build,\nthen point Claude Code at the built binary instead of npx:\n npm v12+: npm install -g @prom.codes/memory-mcp --allow-scripts=better-sqlite3\n npm \u2264 11: npm install -g @prom.codes/memory-mcp --ignore-scripts=false --foreground-scripts\n claude mcp add memory -- node "$(npm root -g)/@prom.codes/memory-mcp/dist/bin.js"\nDocs: https://prom.codes/docs/guides/troubleshooting#could-not-locate-the-bindings-file\n';
|
|
3472
4484
|
async function main() {
|
|
3473
4485
|
const env = process.env;
|
|
3474
4486
|
const explicitRoot = (env.PROMETHEUS_WORKSPACE_ROOT ?? "").trim();
|
|
@@ -3494,6 +4506,8 @@ async function main() {
|
|
|
3494
4506
|
onToolCall: (tool) => heartbeat.update({ lastTool: tool, lastToolCallAt: Date.now() })
|
|
3495
4507
|
});
|
|
3496
4508
|
let watchdog = null;
|
|
4509
|
+
let recorderTimer = null;
|
|
4510
|
+
const RECORDER_INGEST_MS = 6e4;
|
|
3497
4511
|
let shuttingDown = false;
|
|
3498
4512
|
const shutdown = async (reason) => {
|
|
3499
4513
|
if (shuttingDown)
|
|
@@ -3502,6 +4516,8 @@ async function main() {
|
|
|
3502
4516
|
process.stderr.write(`prometheus-memory-mcp: ${reason}, shutting down
|
|
3503
4517
|
`);
|
|
3504
4518
|
watchdog?.stop();
|
|
4519
|
+
if (recorderTimer !== null)
|
|
4520
|
+
clearInterval(recorderTimer);
|
|
3505
4521
|
heartbeat.stop();
|
|
3506
4522
|
try {
|
|
3507
4523
|
await server.close();
|
|
@@ -3558,6 +4574,38 @@ async function main() {
|
|
|
3558
4574
|
});
|
|
3559
4575
|
}
|
|
3560
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?.();
|
|
3561
4609
|
};
|
|
3562
4610
|
if (eagerVia !== null) {
|
|
3563
4611
|
boot(void 0, eagerVia);
|