@prom.codes/memory-mcp 0.11.4 → 0.15.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 +1213 -149
- package/package.json +12 -2
package/dist/bin.js
CHANGED
|
@@ -209,15 +209,15 @@ async function checkForUpdate(options) {
|
|
|
209
209
|
const file = cachePath(cacheDir, name);
|
|
210
210
|
const now = Date.now();
|
|
211
211
|
if (!force) {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
const updateAvailable2 =
|
|
212
|
+
const cached2 = await readCache(file);
|
|
213
|
+
if (cached2 !== null && now - cached2.checkedAt < cacheTtlMs && !cachedLatestIsStale(cached2.latest, version)) {
|
|
214
|
+
const updateAvailable2 = cached2.latest !== null && isNewerVersion(cached2.latest, version);
|
|
215
215
|
if (updateAvailable2)
|
|
216
|
-
notify(log, name, version,
|
|
217
|
-
await syncAvailabilityMarker(cacheDir, name, version,
|
|
216
|
+
notify(log, name, version, cached2.latest);
|
|
217
|
+
await syncAvailabilityMarker(cacheDir, name, version, cached2.latest, updateAvailable2);
|
|
218
218
|
return {
|
|
219
219
|
...base,
|
|
220
|
-
latest:
|
|
220
|
+
latest: cached2.latest,
|
|
221
221
|
checked: false,
|
|
222
222
|
updateAvailable: updateAvailable2,
|
|
223
223
|
reason: "throttled"
|
|
@@ -239,14 +239,14 @@ async function checkForUpdate(options) {
|
|
|
239
239
|
async function getLatestVersion(name, options = {}) {
|
|
240
240
|
const { env = process.env, fetch: fetchImpl = globalThis.fetch, cacheDir = join(homedir(), ".prometheus"), cacheTtlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, minVersion } = options;
|
|
241
241
|
const file = cachePath(cacheDir, name);
|
|
242
|
-
const
|
|
242
|
+
const cached2 = await readCache(file);
|
|
243
243
|
const now = Date.now();
|
|
244
|
-
const stale = cachedLatestIsStale(
|
|
245
|
-
if (
|
|
246
|
-
return
|
|
244
|
+
const stale = cachedLatestIsStale(cached2?.latest ?? null, minVersion);
|
|
245
|
+
if (cached2 !== null && cached2.latest !== null && now - cached2.checkedAt < cacheTtlMs && !stale) {
|
|
246
|
+
return cached2.latest;
|
|
247
247
|
}
|
|
248
248
|
if (OPT_OUT_RE.test(env.PROMETHEUS_NO_UPDATE_CHECK ?? "") || typeof fetchImpl !== "function") {
|
|
249
|
-
return stale ? null :
|
|
249
|
+
return stale ? null : cached2?.latest ?? null;
|
|
250
250
|
}
|
|
251
251
|
const latest = await fetchLatest(name, fetchImpl, timeoutMs);
|
|
252
252
|
if (latest !== null) {
|
|
@@ -254,7 +254,7 @@ async function getLatestVersion(name, options = {}) {
|
|
|
254
254
|
await writeCache(file, { checkedAt: now, latest });
|
|
255
255
|
return latest;
|
|
256
256
|
}
|
|
257
|
-
return stale ? null :
|
|
257
|
+
return stale ? null : cached2?.latest ?? null;
|
|
258
258
|
}
|
|
259
259
|
function notify(log, name, current, latest) {
|
|
260
260
|
log(`${name}: a newer version (${latest}) is available \u2014 you are on ${current}. npx users get it automatically on the next restart; for a global install run \`npm update -g ${name}\`. (Set PROMETHEUS_NO_UPDATE_CHECK=1 to silence.)
|
|
@@ -426,10 +426,58 @@ function createIdleWatchdog(options) {
|
|
|
426
426
|
};
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
// ../shared/dist/native-binding.js
|
|
430
|
+
import { createRequire } from "node:module";
|
|
431
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
432
|
+
import { existsSync } from "node:fs";
|
|
433
|
+
var require_ = createRequire(import.meta.url);
|
|
434
|
+
function isMusl() {
|
|
435
|
+
try {
|
|
436
|
+
const report = process.report?.getReport();
|
|
437
|
+
const header = typeof report === "object" && report !== null ? report : {};
|
|
438
|
+
return header.header?.glibcVersionRuntime === void 0;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function platformToken(platform = process.platform, arch = process.arch, musl = platform === "linux" && isMusl()) {
|
|
444
|
+
const os = platform === "linux" && musl ? "linuxmusl" : platform;
|
|
445
|
+
return `${os}-${arch}`;
|
|
446
|
+
}
|
|
447
|
+
function nativePackageName(token = platformToken()) {
|
|
448
|
+
return `@prom.codes/native-${token}`;
|
|
449
|
+
}
|
|
450
|
+
function addonFileName(abi = process.versions.modules) {
|
|
451
|
+
return `better_sqlite3/node-v${abi}.node`;
|
|
452
|
+
}
|
|
453
|
+
var cached;
|
|
454
|
+
function resolveNativeBinding() {
|
|
455
|
+
if (cached !== void 0)
|
|
456
|
+
return cached ?? void 0;
|
|
457
|
+
cached = null;
|
|
458
|
+
const override = process.env.PROMETHEUS_SQLITE_NATIVE_BINDING?.trim();
|
|
459
|
+
if (override) {
|
|
460
|
+
cached = existsSync(override) ? override : null;
|
|
461
|
+
return cached ?? void 0;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const manifest = require_.resolve(`${nativePackageName()}/package.json`);
|
|
465
|
+
const candidate = join4(dirname2(manifest), addonFileName());
|
|
466
|
+
cached = existsSync(candidate) ? candidate : null;
|
|
467
|
+
} catch {
|
|
468
|
+
cached = null;
|
|
469
|
+
}
|
|
470
|
+
return cached ?? void 0;
|
|
471
|
+
}
|
|
472
|
+
function nativeBindingOption() {
|
|
473
|
+
const p = resolveNativeBinding();
|
|
474
|
+
return p ? { nativeBinding: p } : {};
|
|
475
|
+
}
|
|
476
|
+
|
|
429
477
|
// dist/composition.js
|
|
430
478
|
import { createHash } from "node:crypto";
|
|
431
|
-
import { homedir as
|
|
432
|
-
import { basename, join as
|
|
479
|
+
import { homedir as homedir5 } from "node:os";
|
|
480
|
+
import { basename, join as join7, resolve as resolve2 } from "node:path";
|
|
433
481
|
|
|
434
482
|
// ../embeddings-openai-compat/dist/index.js
|
|
435
483
|
var DEFAULT_BATCH = 96;
|
|
@@ -1548,6 +1596,26 @@ function requireApiKey(env) {
|
|
|
1548
1596
|
|
|
1549
1597
|
// dist/extraction.js
|
|
1550
1598
|
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.';
|
|
1599
|
+
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.';
|
|
1600
|
+
function parseCuration(raw, maxSummaryChars = 2e3) {
|
|
1601
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
1602
|
+
const empty = { summary: "", facts: [], procedures: [] };
|
|
1603
|
+
if (!match)
|
|
1604
|
+
return empty;
|
|
1605
|
+
let parsed;
|
|
1606
|
+
try {
|
|
1607
|
+
parsed = JSON.parse(match[0]);
|
|
1608
|
+
} catch {
|
|
1609
|
+
return empty;
|
|
1610
|
+
}
|
|
1611
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1612
|
+
return empty;
|
|
1613
|
+
const obj = parsed;
|
|
1614
|
+
const summary = typeof obj.summary === "string" ? obj.summary.trim().slice(0, maxSummaryChars) : "";
|
|
1615
|
+
const facts = Array.isArray(obj.facts) ? parseExtraction(JSON.stringify(obj.facts), 10) : [];
|
|
1616
|
+
const procedures = Array.isArray(obj.procedures) ? parseExtraction(JSON.stringify(obj.procedures), 6) : [];
|
|
1617
|
+
return { summary, facts, procedures };
|
|
1618
|
+
}
|
|
1551
1619
|
function parseExtraction(raw, maxFacts = 12, maxValueChars = 2e3) {
|
|
1552
1620
|
const match = raw.match(/\[[\s\S]*\]/);
|
|
1553
1621
|
if (!match)
|
|
@@ -1607,47 +1675,60 @@ var OpenAICompatExtractor = class {
|
|
|
1607
1675
|
const trimmed = text.trim();
|
|
1608
1676
|
if (trimmed === "")
|
|
1609
1677
|
return [];
|
|
1678
|
+
const content = await this.#chat(SYSTEM_PROMPT, `Session notes:
|
|
1679
|
+
|
|
1680
|
+
${trimmed}`, opts?.signal);
|
|
1681
|
+
return content === null ? [] : parseExtraction(content);
|
|
1682
|
+
}
|
|
1683
|
+
async curate(sessionText, opts) {
|
|
1684
|
+
const trimmed = sessionText.trim();
|
|
1685
|
+
const empty = { summary: "", facts: [], procedures: [] };
|
|
1686
|
+
if (trimmed === "")
|
|
1687
|
+
return empty;
|
|
1688
|
+
const content = await this.#chat(CURATION_SYSTEM_PROMPT, `Session log:
|
|
1689
|
+
|
|
1690
|
+
${trimmed}`, opts?.signal);
|
|
1691
|
+
return content === null ? empty : parseCuration(content);
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* One chat-completion round with retry on 429/5xx. Returns the message content,
|
|
1695
|
+
* or `null` on a permanent client error / exhausted retries (callers degrade
|
|
1696
|
+
* gracefully — never throw). Shared by {@link extract} and {@link curate}.
|
|
1697
|
+
*/
|
|
1698
|
+
async #chat(system, user, signal) {
|
|
1610
1699
|
const body = JSON.stringify({
|
|
1611
1700
|
model: this.model,
|
|
1612
1701
|
temperature: this.#temperature,
|
|
1613
1702
|
messages: [
|
|
1614
|
-
{ role: "system", content:
|
|
1615
|
-
{ role: "user", content:
|
|
1616
|
-
|
|
1617
|
-
${trimmed}` }
|
|
1703
|
+
{ role: "system", content: system },
|
|
1704
|
+
{ role: "user", content: user }
|
|
1618
1705
|
]
|
|
1619
1706
|
});
|
|
1620
1707
|
const headers = { "content-type": "application/json" };
|
|
1621
1708
|
if (this.#apiKey !== void 0 && this.#apiKey !== "") {
|
|
1622
1709
|
headers.authorization = `Bearer ${this.#apiKey}`;
|
|
1623
1710
|
}
|
|
1624
|
-
let lastErr;
|
|
1625
1711
|
for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
|
|
1626
1712
|
try {
|
|
1627
1713
|
const res = await this.#fetch(this.#url, {
|
|
1628
1714
|
method: "POST",
|
|
1629
1715
|
headers,
|
|
1630
1716
|
body,
|
|
1631
|
-
...
|
|
1717
|
+
...signal ? { signal } : {}
|
|
1632
1718
|
});
|
|
1633
1719
|
if (res.status === 429 || res.status >= 500) {
|
|
1634
|
-
lastErr = new Error(`extractor HTTP ${res.status}`);
|
|
1635
1720
|
} else if (!res.ok) {
|
|
1636
|
-
return
|
|
1721
|
+
return null;
|
|
1637
1722
|
} else {
|
|
1638
1723
|
const json = await res.json();
|
|
1639
|
-
|
|
1640
|
-
return parseExtraction(content);
|
|
1724
|
+
return json.choices?.[0]?.message?.content ?? "";
|
|
1641
1725
|
}
|
|
1642
|
-
} catch
|
|
1643
|
-
lastErr = err;
|
|
1726
|
+
} catch {
|
|
1644
1727
|
}
|
|
1645
|
-
if (attempt < this.#maxRetries)
|
|
1728
|
+
if (attempt < this.#maxRetries)
|
|
1646
1729
|
await delay(this.#retryBaseMs * 2 ** attempt);
|
|
1647
|
-
}
|
|
1648
1730
|
}
|
|
1649
|
-
|
|
1650
|
-
return [];
|
|
1731
|
+
return null;
|
|
1651
1732
|
}
|
|
1652
1733
|
};
|
|
1653
1734
|
function delay(ms) {
|
|
@@ -1729,10 +1810,347 @@ var OpenAICompatRewriter = class {
|
|
|
1729
1810
|
|
|
1730
1811
|
// dist/sqlite.js
|
|
1731
1812
|
import { randomUUID } from "node:crypto";
|
|
1732
|
-
import { mkdirSync as
|
|
1733
|
-
import { dirname as
|
|
1813
|
+
import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync as rmSync2 } from "node:fs";
|
|
1814
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
1734
1815
|
import Database from "better-sqlite3";
|
|
1735
1816
|
|
|
1817
|
+
// dist/security.js
|
|
1818
|
+
var SECRET_PATTERNS = [
|
|
1819
|
+
{ name: "openai-key", regex: /\bsk-proj-[A-Za-z0-9_-]{20,}/ },
|
|
1820
|
+
{ name: "anthropic-key", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
|
|
1821
|
+
{ name: "supabase-token", regex: /\bsbp?_[A-Za-z0-9]{20,}/ },
|
|
1822
|
+
{ name: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}/ },
|
|
1823
|
+
{ name: "gitlab-token", regex: /\bglpat-[A-Za-z0-9_-]{20,}/ },
|
|
1824
|
+
{ name: "dockerhub-token", regex: /\bdckr_(?:pat|oat)_[A-Za-z0-9_-]{10,}/ },
|
|
1825
|
+
{ name: "resend-key", regex: /\bre_[A-Za-z0-9]{8,}_[A-Za-z0-9]{10,}/ },
|
|
1826
|
+
{ name: "runpod-key", regex: /\brpa_[A-Za-z0-9]{30,}/ },
|
|
1827
|
+
{ name: "sentry-token", regex: /\bsntrys_[A-Za-z0-9+/=_-]{20,}/ },
|
|
1828
|
+
{ name: "vercel-token", regex: /\bvc[kp]_[A-Za-z0-9]{20,}/ },
|
|
1829
|
+
{ name: "huggingface-token", regex: /\bhf_[A-Za-z0-9]{30,}/ },
|
|
1830
|
+
{ name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{30,}/ },
|
|
1831
|
+
{ name: "voyage-key", regex: /\bpa-[A-Za-z0-9_-]{30,}/ },
|
|
1832
|
+
{ name: "google-api-key", regex: /\bAIza[A-Za-z0-9_-]{30,}/ },
|
|
1833
|
+
{ name: "sovrgpt-key", regex: /\bsov_[a-f0-9]{40,}/ },
|
|
1834
|
+
{ name: "prometheus-key", regex: /\bprom_(?:live|test)_[A-Za-z0-9]{10,}/ },
|
|
1835
|
+
{ name: "aws-access-key", regex: /\bAKIA[A-Z0-9]{16}\b/ },
|
|
1836
|
+
{ name: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/ },
|
|
1837
|
+
{ name: "private-key-block", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
1838
|
+
{ name: "authorization-header", regex: /\bAuthorization:\s*(?:Bearer|Basic)\s+\S{8,}/i },
|
|
1839
|
+
{
|
|
1840
|
+
name: "connection-string-credentials",
|
|
1841
|
+
regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s/@:]+:[^\s/@]+@/i
|
|
1842
|
+
}
|
|
1843
|
+
];
|
|
1844
|
+
function findSecretPatterns(text) {
|
|
1845
|
+
const hits = [];
|
|
1846
|
+
for (const p of SECRET_PATTERNS) {
|
|
1847
|
+
if (p.regex.test(text))
|
|
1848
|
+
hits.push(p.name);
|
|
1849
|
+
}
|
|
1850
|
+
return hits;
|
|
1851
|
+
}
|
|
1852
|
+
var SECRET_VALUE_ERROR = "memory value matches the secret deny-list and was rejected";
|
|
1853
|
+
function assertNoSecrets(text) {
|
|
1854
|
+
const hits = findSecretPatterns(text);
|
|
1855
|
+
if (hits.length > 0) {
|
|
1856
|
+
throw new Error(`${SECRET_VALUE_ERROR} (pattern: ${hits.join(", ")}).`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// dist/recorder.js
|
|
1861
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1862
|
+
import { homedir as homedir4 } from "node:os";
|
|
1863
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
1864
|
+
var SPOOL_VERSION = 1;
|
|
1865
|
+
var SECRET_PATTERN_SOURCES = [
|
|
1866
|
+
"sk-proj-[A-Za-z0-9_-]{20,}",
|
|
1867
|
+
"sk-ant-[A-Za-z0-9_-]{20,}",
|
|
1868
|
+
"\\bsbp?_[A-Za-z0-9]{20,}",
|
|
1869
|
+
"\\bgh[pousr]_[A-Za-z0-9]{20,}",
|
|
1870
|
+
"\\bglpat-[A-Za-z0-9_-]{20,}",
|
|
1871
|
+
"\\bdckr_(?:pat|oat)_[A-Za-z0-9_-]{10,}",
|
|
1872
|
+
"\\bre_[A-Za-z0-9]{8,}_[A-Za-z0-9]{10,}",
|
|
1873
|
+
"\\brpa_[A-Za-z0-9]{30,}",
|
|
1874
|
+
"\\bsntrys_[A-Za-z0-9+/=_-]{20,}",
|
|
1875
|
+
"\\bvc[kp]_[A-Za-z0-9]{20,}",
|
|
1876
|
+
"\\bhf_[A-Za-z0-9]{30,}",
|
|
1877
|
+
"\\bnpm_[A-Za-z0-9]{30,}",
|
|
1878
|
+
"\\bpa-[A-Za-z0-9_-]{30,}",
|
|
1879
|
+
"\\bAIza[A-Za-z0-9_-]{30,}",
|
|
1880
|
+
"\\bsov_[a-f0-9]{40,}",
|
|
1881
|
+
"\\bprom_(?:live|test)_[A-Za-z0-9]{10,}",
|
|
1882
|
+
"\\bAKIA[A-Z0-9]{16}\\b",
|
|
1883
|
+
"\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{5,}",
|
|
1884
|
+
"-----BEGIN [A-Z ]*PRIVATE KEY-----",
|
|
1885
|
+
"\\b(?:postgres(?:ql)?|mysql|mongodb(?:\\+srv)?|redis|amqp)://[^\\s/@:]+:[^\\s/@]+@"
|
|
1886
|
+
];
|
|
1887
|
+
var REDACT_RE = new RegExp(`(${SECRET_PATTERN_SOURCES.join(")|(")})`, "gi");
|
|
1888
|
+
var EVENT_CAP_BYTES = 4 * 1024;
|
|
1889
|
+
var SPOOL_CAP_BYTES = 4 * 1024 * 1024;
|
|
1890
|
+
function recorderRoot(env = process.env) {
|
|
1891
|
+
const base = env.PROMETHEUS_DIR && env.PROMETHEUS_DIR !== "" ? env.PROMETHEUS_DIR : join5(homedir4(), ".prometheus");
|
|
1892
|
+
return join5(base, "recorder");
|
|
1893
|
+
}
|
|
1894
|
+
function sanitizeSegment(s) {
|
|
1895
|
+
return s.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128) || "_";
|
|
1896
|
+
}
|
|
1897
|
+
function parseSpool(raw) {
|
|
1898
|
+
const lines = raw.split("\n");
|
|
1899
|
+
const events = [];
|
|
1900
|
+
let droppedPartialTail = false;
|
|
1901
|
+
let ended = false;
|
|
1902
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1903
|
+
const line = lines[i];
|
|
1904
|
+
if (line.trim() === "")
|
|
1905
|
+
continue;
|
|
1906
|
+
try {
|
|
1907
|
+
const obj = JSON.parse(line);
|
|
1908
|
+
if (obj && typeof obj.event === "string" && typeof obj.sessionId === "string") {
|
|
1909
|
+
events.push({ ...obj, seq: i });
|
|
1910
|
+
if (obj.event === "session_end")
|
|
1911
|
+
ended = true;
|
|
1912
|
+
}
|
|
1913
|
+
} catch {
|
|
1914
|
+
if (i === lines.length - 1)
|
|
1915
|
+
droppedPartialTail = true;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return { events, droppedPartialTail, ended };
|
|
1919
|
+
}
|
|
1920
|
+
var RECORDER_HOOK_FILENAME = "prometheus-recorder-hook.mjs";
|
|
1921
|
+
var RECORDER_HOOK_SCRIPT = String.raw`#!/usr/bin/env node
|
|
1922
|
+
// Prometheus Session Recorder hook (Claude Code). Appends ONE diet+redacted
|
|
1923
|
+
// JSONL event per invocation to ~/.prometheus/recorder/<projectId>/<sessionId>.jsonl.
|
|
1924
|
+
// Pure node stdlib, no deps. EXITS 0 ON EVERY PATH — a recorder failure must
|
|
1925
|
+
// never disturb a coding session. Managed by prom.codes (memory-mcp recorder_setup).
|
|
1926
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
1927
|
+
import { homedir } from "node:os";
|
|
1928
|
+
import { join } from "node:path";
|
|
1929
|
+
import { createHash } from "node:crypto";
|
|
1930
|
+
|
|
1931
|
+
const SPOOL_CAP = ${SPOOL_CAP_BYTES};
|
|
1932
|
+
const EVENT_CAP = ${EVENT_CAP_BYTES};
|
|
1933
|
+
const SECRET_SOURCES = ${JSON.stringify(SECRET_PATTERN_SOURCES)};
|
|
1934
|
+
const REDACT = new RegExp("(" + SECRET_SOURCES.join(")|(") + ")", "gi");
|
|
1935
|
+
|
|
1936
|
+
function redact(s) { return String(s).replace(REDACT, "[redacted]"); }
|
|
1937
|
+
function clip(s, n) {
|
|
1938
|
+
const str = typeof s === "string" ? s : (s === undefined ? "" : JSON.stringify(s));
|
|
1939
|
+
return str.length > n ? str.slice(0, n) + "…" : str;
|
|
1940
|
+
}
|
|
1941
|
+
function sanitize(s) { return String(s).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128) || "_"; }
|
|
1942
|
+
function sensitivePath(p) {
|
|
1943
|
+
const s = String(p).replace(/\\/g, "/").toLowerCase();
|
|
1944
|
+
const base = s.slice(s.lastIndexOf("/") + 1);
|
|
1945
|
+
if (/^\.env(\..+)?$/.test(base)) return true;
|
|
1946
|
+
if (base === "id_rsa" || base === "id_dsa" || base === "id_ecdsa" || base === "id_ed25519") return true;
|
|
1947
|
+
if (/\.(pem|key|pfx|p12|keystore)$/.test(base)) return true;
|
|
1948
|
+
if (/(^|\/)(secrets?|credentials?)(\/|$)/.test(s)) return true;
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
function projectIdOf(root) { return createHash("sha256").update(String(root)).digest("hex").slice(0, 16); }
|
|
1952
|
+
|
|
1953
|
+
function dietTool(toolName, inp, result) {
|
|
1954
|
+
const tool = toolName; inp = inp || {};
|
|
1955
|
+
const fp = typeof inp.file_path === "string" ? inp.file_path : undefined;
|
|
1956
|
+
if (fp !== undefined && sensitivePath(fp)) return { tool: tool, note: "[skipped: sensitive path]" };
|
|
1957
|
+
if (tool === "Edit" || tool === "Write" || tool === "NotebookEdit") {
|
|
1958
|
+
return { tool: tool, file_path: fp, oldPreview: clip(inp.old_string || "", 100), newPreview: clip(inp.new_string || inp.content || "", 100) };
|
|
1959
|
+
}
|
|
1960
|
+
if (tool === "Bash") {
|
|
1961
|
+
const r = result || {};
|
|
1962
|
+
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) };
|
|
1963
|
+
}
|
|
1964
|
+
if (tool === "Read" || tool === "Glob" || tool === "Grep" || tool === "LS") {
|
|
1965
|
+
return { tool: tool, target: clip(inp.file_path || inp.path || inp.pattern || "", 200) };
|
|
1966
|
+
}
|
|
1967
|
+
return { tool: tool, resultPreview: clip(result, 500) };
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function finalize(payload) {
|
|
1971
|
+
const s = redact(JSON.stringify(payload));
|
|
1972
|
+
return s.length > EVENT_CAP ? s.slice(0, EVENT_CAP) + "…" : s;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// Read the last assistant text from a Claude Code transcript JSONL (best-effort).
|
|
1976
|
+
function lastAssistantText(transcriptPath) {
|
|
1977
|
+
try {
|
|
1978
|
+
if (!transcriptPath) return "";
|
|
1979
|
+
const raw = readFileSync(transcriptPath, "utf8");
|
|
1980
|
+
const lines = raw.split("\n");
|
|
1981
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1982
|
+
const ln = lines[i].trim(); if (ln === "") continue;
|
|
1983
|
+
let obj; try { obj = JSON.parse(ln); } catch { continue; }
|
|
1984
|
+
const msg = obj && obj.message ? obj.message : obj;
|
|
1985
|
+
const role = obj && obj.type ? obj.type : (msg && msg.role);
|
|
1986
|
+
if (role === "assistant" && msg) {
|
|
1987
|
+
const c = msg.content;
|
|
1988
|
+
if (typeof c === "string") return c;
|
|
1989
|
+
if (Array.isArray(c)) {
|
|
1990
|
+
const txt = c.filter(function (b) { return b && b.type === "text"; }).map(function (b) { return b.text; }).join("\n");
|
|
1991
|
+
if (txt) return txt;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
} catch { /* best-effort */ }
|
|
1996
|
+
return "";
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function run(input) {
|
|
2000
|
+
let p = {};
|
|
2001
|
+
try { p = JSON.parse(String(input || "").replace(/^/, "").trim() || "{}"); } catch { return; }
|
|
2002
|
+
const eventName = typeof p.hook_event_name === "string" ? p.hook_event_name : "";
|
|
2003
|
+
const sessionId = typeof p.session_id === "string" && p.session_id !== "" ? p.session_id : "unknown";
|
|
2004
|
+
const root = (process.env.CLAUDE_PROJECT_DIR && process.env.CLAUDE_PROJECT_DIR !== "") ? process.env.CLAUDE_PROJECT_DIR : (p.cwd || process.cwd());
|
|
2005
|
+
const projectId = projectIdOf(root);
|
|
2006
|
+
|
|
2007
|
+
let event = null; let payload = {};
|
|
2008
|
+
if (eventName === "SessionStart") { event = "session_start"; payload = { cwd: root, model: p.model || null, source: p.source || null }; }
|
|
2009
|
+
else if (eventName === "UserPromptSubmit") { event = "user_message"; payload = { text: clip(p.prompt || "", 4000) }; }
|
|
2010
|
+
else if (eventName === "PostToolUse") { event = "tool_use"; payload = dietTool(p.tool_name || "", p.tool_input, p.tool_response); }
|
|
2011
|
+
else if (eventName === "Stop" || eventName === "SubagentStop") { event = "assistant_message"; payload = { text: clip(lastAssistantText(p.transcript_path || ""), 4000) }; }
|
|
2012
|
+
else if (eventName === "SessionEnd") { event = "session_end"; payload = { reason: p.reason || null }; }
|
|
2013
|
+
else return;
|
|
2014
|
+
|
|
2015
|
+
const base = join((process.env.PROMETHEUS_DIR && process.env.PROMETHEUS_DIR !== "") ? process.env.PROMETHEUS_DIR : join(homedir(), ".prometheus"), "recorder", sanitize(projectId));
|
|
2016
|
+
const file = join(base, sanitize(sessionId) + ".jsonl");
|
|
2017
|
+
|
|
2018
|
+
// Per-session cap: once the spool is large, keep session_end + messages but
|
|
2019
|
+
// drop further tool_use (the noisiest, most numerous events).
|
|
2020
|
+
try {
|
|
2021
|
+
if (event === "tool_use") {
|
|
2022
|
+
let size = 0; try { size = statSync(file).size; } catch { size = 0; }
|
|
2023
|
+
if (size >= SPOOL_CAP) return;
|
|
2024
|
+
}
|
|
2025
|
+
} catch { /* stat failure -> keep going */ }
|
|
2026
|
+
|
|
2027
|
+
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";
|
|
2028
|
+
try { mkdirSync(base, { recursive: true }); appendFileSync(file, line, { encoding: "utf8", flag: "a" }); } catch { /* never throw */ }
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
if (process.stdin.isTTY) { process.exit(0); }
|
|
2032
|
+
else {
|
|
2033
|
+
let raw = "";
|
|
2034
|
+
process.stdin.on("data", function (c) { raw += c; });
|
|
2035
|
+
process.stdin.on("end", function () { try { run(raw); } catch { /* swallow */ } process.exit(0); });
|
|
2036
|
+
process.stdin.on("error", function () { process.exit(0); });
|
|
2037
|
+
}
|
|
2038
|
+
`;
|
|
2039
|
+
var RECORDER_EVENTS = [
|
|
2040
|
+
"SessionStart",
|
|
2041
|
+
"UserPromptSubmit",
|
|
2042
|
+
"PostToolUse",
|
|
2043
|
+
"Stop",
|
|
2044
|
+
"SessionEnd"
|
|
2045
|
+
];
|
|
2046
|
+
var RECORDER_HOOK_TIMEOUT = 5;
|
|
2047
|
+
function resolveSettingsPath(opts) {
|
|
2048
|
+
if (opts.settingsPathOverride)
|
|
2049
|
+
return opts.settingsPathOverride;
|
|
2050
|
+
const root = opts.projectRoot ?? process.cwd();
|
|
2051
|
+
if (opts.scope === "project")
|
|
2052
|
+
return join5(root, ".claude", "settings.json");
|
|
2053
|
+
if (opts.scope === "project-local")
|
|
2054
|
+
return join5(root, ".claude", "settings.local.json");
|
|
2055
|
+
return join5(homedir4(), ".claude", "settings.json");
|
|
2056
|
+
}
|
|
2057
|
+
function resolveHookPath(opts) {
|
|
2058
|
+
const dir = opts.hookDirOverride ?? join5(homedir4(), ".prometheus", "hooks");
|
|
2059
|
+
return join5(dir, RECORDER_HOOK_FILENAME);
|
|
2060
|
+
}
|
|
2061
|
+
function ownsEntry(entry) {
|
|
2062
|
+
return Array.isArray(entry.hooks) && entry.hooks.some((h) => typeof h?.command === "string" && h.command.includes(RECORDER_HOOK_FILENAME));
|
|
2063
|
+
}
|
|
2064
|
+
function readSettings(settingsPath) {
|
|
2065
|
+
if (!existsSync2(settingsPath))
|
|
2066
|
+
return {};
|
|
2067
|
+
const raw = readFileSync2(settingsPath, "utf8").replace(/^/, "");
|
|
2068
|
+
if (raw.trim() === "")
|
|
2069
|
+
return {};
|
|
2070
|
+
const parsed = JSON.parse(raw);
|
|
2071
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
2072
|
+
throw new Error(`${settingsPath} does not contain a JSON object.`);
|
|
2073
|
+
}
|
|
2074
|
+
return parsed;
|
|
2075
|
+
}
|
|
2076
|
+
function backupSettings(settingsPath) {
|
|
2077
|
+
if (!existsSync2(settingsPath))
|
|
2078
|
+
return null;
|
|
2079
|
+
const d = /* @__PURE__ */ new Date();
|
|
2080
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
2081
|
+
const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
2082
|
+
const bak = `${settingsPath}.prom-backup-${stamp}`;
|
|
2083
|
+
copyFileSync(settingsPath, bak);
|
|
2084
|
+
return bak;
|
|
2085
|
+
}
|
|
2086
|
+
function recorderStatus(opts = {}) {
|
|
2087
|
+
const settingsPath = resolveSettingsPath(opts);
|
|
2088
|
+
const hookPath = resolveHookPath(opts);
|
|
2089
|
+
let installedEvents = [];
|
|
2090
|
+
try {
|
|
2091
|
+
const settings = readSettings(settingsPath);
|
|
2092
|
+
for (const ev of RECORDER_EVENTS) {
|
|
2093
|
+
const arr = settings.hooks?.[ev];
|
|
2094
|
+
if (Array.isArray(arr) && arr.some(ownsEntry))
|
|
2095
|
+
installedEvents.push(ev);
|
|
2096
|
+
}
|
|
2097
|
+
} catch {
|
|
2098
|
+
installedEvents = [];
|
|
2099
|
+
}
|
|
2100
|
+
return {
|
|
2101
|
+
installed: installedEvents.length > 0,
|
|
2102
|
+
scope: opts.scope ?? "user",
|
|
2103
|
+
events: installedEvents,
|
|
2104
|
+
settingsPath,
|
|
2105
|
+
hookScriptPresent: existsSync2(hookPath)
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function applyRecorderHooks(opts = {}) {
|
|
2109
|
+
const scope = opts.scope ?? "user";
|
|
2110
|
+
const settingsPath = resolveSettingsPath(opts);
|
|
2111
|
+
const hookPath = resolveHookPath(opts);
|
|
2112
|
+
const command = `node "${hookPath.replace(/\\/g, "/")}"`;
|
|
2113
|
+
const settings = readSettings(settingsPath);
|
|
2114
|
+
const backup = backupSettings(settingsPath);
|
|
2115
|
+
settings.hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
|
|
2116
|
+
for (const ev of RECORDER_EVENTS) {
|
|
2117
|
+
const arr = settings.hooks[ev];
|
|
2118
|
+
if (Array.isArray(arr)) {
|
|
2119
|
+
const kept = arr.filter((e) => !ownsEntry(e));
|
|
2120
|
+
if (kept.length === 0)
|
|
2121
|
+
delete settings.hooks[ev];
|
|
2122
|
+
else
|
|
2123
|
+
settings.hooks[ev] = kept;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
if (!opts.uninstall) {
|
|
2127
|
+
mkdirSync2(dirname3(hookPath), { recursive: true });
|
|
2128
|
+
writeFileSync2(hookPath, RECORDER_HOOK_SCRIPT, "utf8");
|
|
2129
|
+
for (const ev of RECORDER_EVENTS) {
|
|
2130
|
+
const matcher = ev === "PostToolUse" ? "*" : "";
|
|
2131
|
+
(settings.hooks[ev] ??= []).push({
|
|
2132
|
+
matcher,
|
|
2133
|
+
hooks: [{ type: "command", command, timeout: RECORDER_HOOK_TIMEOUT }]
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0)
|
|
2138
|
+
delete settings.hooks;
|
|
2139
|
+
mkdirSync2(dirname3(settingsPath), { recursive: true });
|
|
2140
|
+
writeFileSync2(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
2141
|
+
`, "utf8");
|
|
2142
|
+
JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
2143
|
+
return {
|
|
2144
|
+
action: opts.uninstall ? "uninstalled" : "installed",
|
|
2145
|
+
settingsPath,
|
|
2146
|
+
scope,
|
|
2147
|
+
hookPath,
|
|
2148
|
+
events: opts.uninstall ? [] : [...RECORDER_EVENTS],
|
|
2149
|
+
backup,
|
|
2150
|
+
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 }."
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
|
|
1736
2154
|
// dist/rrf.js
|
|
1737
2155
|
function reciprocalRankFusion(lists, options = {}) {
|
|
1738
2156
|
const k = options.k ?? 60;
|
|
@@ -1993,6 +2411,54 @@ CREATE TRIGGER IF NOT EXISTS agent_memory_vec_ad AFTER DELETE ON agent_memory BE
|
|
|
1993
2411
|
DELETE FROM agent_memory_vec WHERE record_id = old.id;
|
|
1994
2412
|
END;
|
|
1995
2413
|
`;
|
|
2414
|
+
var RECORDER_SCHEMA = `
|
|
2415
|
+
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
2416
|
+
session_id TEXT NOT NULL,
|
|
2417
|
+
project_id TEXT NOT NULL,
|
|
2418
|
+
agent TEXT NOT NULL,
|
|
2419
|
+
cwd TEXT,
|
|
2420
|
+
model TEXT,
|
|
2421
|
+
started_at TEXT NOT NULL,
|
|
2422
|
+
ended_at TEXT,
|
|
2423
|
+
stats TEXT,
|
|
2424
|
+
summary TEXT,
|
|
2425
|
+
curated_at TEXT,
|
|
2426
|
+
PRIMARY KEY (project_id, session_id)
|
|
2427
|
+
);
|
|
2428
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON agent_sessions (project_id, started_at DESC);
|
|
2429
|
+
|
|
2430
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
2431
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2432
|
+
project_id TEXT NOT NULL,
|
|
2433
|
+
session_id TEXT NOT NULL,
|
|
2434
|
+
seq INTEGER NOT NULL,
|
|
2435
|
+
ts TEXT NOT NULL,
|
|
2436
|
+
event_type TEXT NOT NULL,
|
|
2437
|
+
tool_name TEXT,
|
|
2438
|
+
content TEXT NOT NULL,
|
|
2439
|
+
metadata TEXT,
|
|
2440
|
+
UNIQUE (project_id, session_id, seq)
|
|
2441
|
+
);
|
|
2442
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON session_events (project_id, session_id, seq);
|
|
2443
|
+
`;
|
|
2444
|
+
var RECORDER_FTS_SCHEMA = `
|
|
2445
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_events_fts USING fts5(
|
|
2446
|
+
content, tool_name,
|
|
2447
|
+
content='session_events',
|
|
2448
|
+
content_rowid='id',
|
|
2449
|
+
tokenize='unicode61'
|
|
2450
|
+
);
|
|
2451
|
+
CREATE TRIGGER IF NOT EXISTS session_events_ai AFTER INSERT ON session_events BEGIN
|
|
2452
|
+
INSERT INTO session_events_fts (rowid, content, tool_name)
|
|
2453
|
+
VALUES (new.id, new.content, new.tool_name);
|
|
2454
|
+
END;
|
|
2455
|
+
CREATE TRIGGER IF NOT EXISTS session_events_ad AFTER DELETE ON session_events BEGIN
|
|
2456
|
+
INSERT INTO session_events_fts (session_events_fts, rowid, content, tool_name)
|
|
2457
|
+
VALUES ('delete', old.id, old.content, old.tool_name);
|
|
2458
|
+
END;
|
|
2459
|
+
`;
|
|
2460
|
+
var DEFAULT_RETENTION_DAYS = 90;
|
|
2461
|
+
var DEFAULT_MAX_SESSIONS = 300;
|
|
1996
2462
|
function vectorToBlob(vector) {
|
|
1997
2463
|
return Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
1998
2464
|
}
|
|
@@ -2043,6 +2509,70 @@ function rowToRecord(row) {
|
|
|
2043
2509
|
updatedAt: row.updated_at
|
|
2044
2510
|
};
|
|
2045
2511
|
}
|
|
2512
|
+
function sessionRowToRecord(r) {
|
|
2513
|
+
return {
|
|
2514
|
+
sessionId: r.session_id,
|
|
2515
|
+
projectId: r.project_id,
|
|
2516
|
+
agent: r.agent,
|
|
2517
|
+
cwd: r.cwd,
|
|
2518
|
+
model: r.model,
|
|
2519
|
+
startedAt: r.started_at,
|
|
2520
|
+
endedAt: r.ended_at,
|
|
2521
|
+
stats: r.stats ? JSON.parse(r.stats) : null,
|
|
2522
|
+
summary: r.summary,
|
|
2523
|
+
curatedAt: r.curated_at
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
function strField(payload, key) {
|
|
2527
|
+
const v = payload[key];
|
|
2528
|
+
return typeof v === "string" ? v : null;
|
|
2529
|
+
}
|
|
2530
|
+
function recorderContent(ev) {
|
|
2531
|
+
let content;
|
|
2532
|
+
let toolName = null;
|
|
2533
|
+
if (ev.event === "user_message" || ev.event === "assistant_message") {
|
|
2534
|
+
content = strField(ev.payload, "text") ?? "";
|
|
2535
|
+
} else if (ev.event === "tool_use") {
|
|
2536
|
+
toolName = strField(ev.payload, "tool");
|
|
2537
|
+
content = JSON.stringify(ev.payload);
|
|
2538
|
+
} else {
|
|
2539
|
+
content = JSON.stringify(ev.payload);
|
|
2540
|
+
}
|
|
2541
|
+
if (findSecretPatterns(content).length > 0)
|
|
2542
|
+
content = "[dropped: secret-like]";
|
|
2543
|
+
return { content, toolName };
|
|
2544
|
+
}
|
|
2545
|
+
function computeSessionStats(evs) {
|
|
2546
|
+
let toolCount = 0;
|
|
2547
|
+
const tools = /* @__PURE__ */ new Set();
|
|
2548
|
+
const files = /* @__PURE__ */ new Set();
|
|
2549
|
+
for (const ev of evs) {
|
|
2550
|
+
if (ev.event !== "tool_use")
|
|
2551
|
+
continue;
|
|
2552
|
+
toolCount++;
|
|
2553
|
+
const t = strField(ev.payload, "tool");
|
|
2554
|
+
if (t !== null)
|
|
2555
|
+
tools.add(t);
|
|
2556
|
+
const fp = strField(ev.payload, "file_path");
|
|
2557
|
+
if (fp !== null)
|
|
2558
|
+
files.add(fp);
|
|
2559
|
+
}
|
|
2560
|
+
return { toolCount, toolsUsed: [...tools], filesTouched: [...files] };
|
|
2561
|
+
}
|
|
2562
|
+
function condenseEvent(r) {
|
|
2563
|
+
let content = r.content;
|
|
2564
|
+
if (r.event_type === "tool_use") {
|
|
2565
|
+
content = r.tool_name ? `[tool] ${r.tool_name}: ${r.content}` : `[tool] ${r.content}`;
|
|
2566
|
+
}
|
|
2567
|
+
return { seq: r.seq, ts: r.ts, eventType: r.event_type, toolName: r.tool_name, content };
|
|
2568
|
+
}
|
|
2569
|
+
function intFromEnv(env, name, def) {
|
|
2570
|
+
const raw = env[name];
|
|
2571
|
+
if (raw === void 0 || raw === "")
|
|
2572
|
+
return def;
|
|
2573
|
+
const n = Number.parseInt(raw, 10);
|
|
2574
|
+
return Number.isFinite(n) && n > 0 ? n : def;
|
|
2575
|
+
}
|
|
2046
2576
|
var SqliteMemoryBackend = class {
|
|
2047
2577
|
db;
|
|
2048
2578
|
embedder;
|
|
@@ -2057,14 +2587,16 @@ var SqliteMemoryBackend = class {
|
|
|
2057
2587
|
closed = false;
|
|
2058
2588
|
constructor(dbPath, opts = {}) {
|
|
2059
2589
|
if (dbPath !== ":memory:") {
|
|
2060
|
-
|
|
2590
|
+
mkdirSync3(dirname4(dbPath), { recursive: true });
|
|
2061
2591
|
}
|
|
2062
|
-
this.db = new Database(dbPath);
|
|
2592
|
+
this.db = new Database(dbPath, { ...nativeBindingOption() });
|
|
2063
2593
|
this.db.pragma("journal_mode = WAL");
|
|
2064
2594
|
this.db.pragma("synchronous = NORMAL");
|
|
2065
2595
|
this.db.exec(SCHEMA);
|
|
2066
2596
|
this.db.exec(FTS_SCHEMA);
|
|
2067
2597
|
this.db.exec(VEC_SCHEMA);
|
|
2598
|
+
this.db.exec(RECORDER_SCHEMA);
|
|
2599
|
+
this.db.exec(RECORDER_FTS_SCHEMA);
|
|
2068
2600
|
this.db.exec(`INSERT INTO agent_memory_fts (agent_memory_fts) VALUES ('rebuild')`);
|
|
2069
2601
|
this.embedder = opts.embedder;
|
|
2070
2602
|
this.reranker = opts.reranker;
|
|
@@ -2492,6 +3024,183 @@ ${h.record.value}`
|
|
|
2492
3024
|
this.audit("consolidate", { scope: input.scope, scopeId: input.scopeId }, `records=${written.length}`);
|
|
2493
3025
|
return written;
|
|
2494
3026
|
}
|
|
3027
|
+
// ===================================================================
|
|
3028
|
+
// Session Recorder (M1) — RecorderStore implementation.
|
|
3029
|
+
// ===================================================================
|
|
3030
|
+
/**
|
|
3031
|
+
* Ingest every spool file for `projectId` under the recorder root: parse,
|
|
3032
|
+
* write events idempotently (`INSERT OR IGNORE` on `(project_id, session_id,
|
|
3033
|
+
* seq)`), maintain the session header, and DELETE the spool of any session that
|
|
3034
|
+
* has ended (its `session_end` is persisted). Best-effort per file — a corrupt
|
|
3035
|
+
* spool never aborts the sweep. Returns roll-up counts.
|
|
3036
|
+
*/
|
|
3037
|
+
async ingestSpoolDir(projectId, env = process.env) {
|
|
3038
|
+
const dir = join6(recorderRoot(env), sanitizeSegment(projectId));
|
|
3039
|
+
let sessions = 0;
|
|
3040
|
+
let events = 0;
|
|
3041
|
+
let deletedSpools = 0;
|
|
3042
|
+
let files;
|
|
3043
|
+
try {
|
|
3044
|
+
files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl"));
|
|
3045
|
+
} catch {
|
|
3046
|
+
return { sessions: 0, events: 0, deletedSpools: 0 };
|
|
3047
|
+
}
|
|
3048
|
+
for (const file of files) {
|
|
3049
|
+
const full = join6(dir, file);
|
|
3050
|
+
try {
|
|
3051
|
+
const raw = readFileSync3(full, "utf8");
|
|
3052
|
+
const parsed = parseSpool(raw);
|
|
3053
|
+
if (parsed.events.length === 0) {
|
|
3054
|
+
continue;
|
|
3055
|
+
}
|
|
3056
|
+
const written = this.ingestSessionEvents(projectId, parsed.events);
|
|
3057
|
+
events += written;
|
|
3058
|
+
sessions += 1;
|
|
3059
|
+
if (parsed.ended) {
|
|
3060
|
+
try {
|
|
3061
|
+
rmSync2(full, { force: true });
|
|
3062
|
+
deletedSpools += 1;
|
|
3063
|
+
} catch {
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
} catch {
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
return { sessions, events, deletedSpools };
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
3072
|
+
* Write one session's parsed events: ensure the header, insert events
|
|
3073
|
+
* idempotently (re-scanning each content for secrets → `[dropped]`), and set
|
|
3074
|
+
* cwd/model (from `session_start`) + ended_at/stats (from `session_end`).
|
|
3075
|
+
* Runs in a single transaction. Returns the number of NEW event rows.
|
|
3076
|
+
*/
|
|
3077
|
+
ingestSessionEvents(projectId, evs) {
|
|
3078
|
+
if (evs.length === 0)
|
|
3079
|
+
return 0;
|
|
3080
|
+
const sessionId = evs[0].sessionId;
|
|
3081
|
+
const agent = evs[0].agent || "claude-code";
|
|
3082
|
+
const firstTs = evs[0].ts;
|
|
3083
|
+
const ensureHeader = this.db.prepare(`INSERT OR IGNORE INTO agent_sessions (session_id, project_id, agent, started_at)
|
|
3084
|
+
VALUES (?, ?, ?, ?)`);
|
|
3085
|
+
const insertEvent = this.db.prepare(`INSERT OR IGNORE INTO session_events
|
|
3086
|
+
(project_id, session_id, seq, ts, event_type, tool_name, content, metadata)
|
|
3087
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
3088
|
+
const setStart = this.db.prepare(`UPDATE agent_sessions SET cwd = ?, model = ?, started_at = ? WHERE project_id = ? AND session_id = ?`);
|
|
3089
|
+
const setEnd = this.db.prepare(`UPDATE agent_sessions SET ended_at = ?, stats = ? WHERE project_id = ? AND session_id = ?`);
|
|
3090
|
+
let written = 0;
|
|
3091
|
+
const tx = this.db.transaction(() => {
|
|
3092
|
+
ensureHeader.run(sessionId, projectId, agent, firstTs);
|
|
3093
|
+
for (const ev of evs) {
|
|
3094
|
+
const { content, toolName } = recorderContent(ev);
|
|
3095
|
+
const res = insertEvent.run(projectId, sessionId, ev.seq, ev.ts, ev.event, toolName, content, null);
|
|
3096
|
+
written += res.changes;
|
|
3097
|
+
if (ev.event === "session_start") {
|
|
3098
|
+
const cwd = strField(ev.payload, "cwd");
|
|
3099
|
+
const model = strField(ev.payload, "model");
|
|
3100
|
+
setStart.run(cwd, model, ev.ts, projectId, sessionId);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
const end = evs.find((e) => e.event === "session_end");
|
|
3104
|
+
if (end !== void 0) {
|
|
3105
|
+
setEnd.run(end.ts, JSON.stringify(computeSessionStats(evs)), projectId, sessionId);
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
tx();
|
|
3109
|
+
return written;
|
|
3110
|
+
}
|
|
3111
|
+
async listSessions(projectId, limit = 20) {
|
|
3112
|
+
const rows = this.db.prepare(`SELECT * FROM agent_sessions WHERE project_id = ? ORDER BY started_at DESC LIMIT ?`).all(projectId, limit);
|
|
3113
|
+
return rows.map(sessionRowToRecord);
|
|
3114
|
+
}
|
|
3115
|
+
async getSession(projectId, sessionId, maxChars = 2e4) {
|
|
3116
|
+
const header = this.db.prepare(`SELECT * FROM agent_sessions WHERE project_id = ? AND session_id = ?`).get(projectId, sessionId);
|
|
3117
|
+
if (header === void 0)
|
|
3118
|
+
return null;
|
|
3119
|
+
const rows = this.db.prepare(`SELECT seq, ts, event_type, tool_name, content FROM session_events
|
|
3120
|
+
WHERE project_id = ? AND session_id = ? ORDER BY seq ASC`).all(projectId, sessionId);
|
|
3121
|
+
const events = [];
|
|
3122
|
+
let used = 0;
|
|
3123
|
+
let truncated = false;
|
|
3124
|
+
for (const r of rows) {
|
|
3125
|
+
const line = condenseEvent(r);
|
|
3126
|
+
if (used + line.content.length > maxChars && events.length > 0) {
|
|
3127
|
+
truncated = true;
|
|
3128
|
+
break;
|
|
3129
|
+
}
|
|
3130
|
+
used += line.content.length;
|
|
3131
|
+
events.push(line);
|
|
3132
|
+
}
|
|
3133
|
+
return { session: sessionRowToRecord(header), events, truncated };
|
|
3134
|
+
}
|
|
3135
|
+
async searchSessions(projectId, query, limit = 20) {
|
|
3136
|
+
const match = toFtsQuery(query);
|
|
3137
|
+
if (match === "")
|
|
3138
|
+
return [];
|
|
3139
|
+
const rows = this.db.prepare(`SELECT e.session_id AS session_id, e.ts AS ts, e.event_type AS event_type,
|
|
3140
|
+
snippet(session_events_fts, 0, '\xAB', '\xBB', ' \u2026 ', 12) AS snip
|
|
3141
|
+
FROM session_events_fts f
|
|
3142
|
+
JOIN session_events e ON e.id = f.rowid
|
|
3143
|
+
WHERE session_events_fts MATCH ? AND e.project_id = ?
|
|
3144
|
+
ORDER BY rank LIMIT ?`).all(match, projectId, limit);
|
|
3145
|
+
return rows.map((r) => ({
|
|
3146
|
+
sessionId: r.session_id,
|
|
3147
|
+
ts: r.ts,
|
|
3148
|
+
eventType: r.event_type,
|
|
3149
|
+
snippet: r.snip
|
|
3150
|
+
}));
|
|
3151
|
+
}
|
|
3152
|
+
/** M2: sessions that ended but have not been curated yet (newest first). */
|
|
3153
|
+
async listUncuratedSessions(projectId, limit = 10) {
|
|
3154
|
+
const rows = this.db.prepare(`SELECT * FROM agent_sessions
|
|
3155
|
+
WHERE project_id = ? AND ended_at IS NOT NULL AND curated_at IS NULL
|
|
3156
|
+
ORDER BY started_at DESC LIMIT ?`).all(projectId, limit);
|
|
3157
|
+
return rows.map(sessionRowToRecord);
|
|
3158
|
+
}
|
|
3159
|
+
/** M2: store a session's distilled summary + stamp `curated_at` (idempotency gate). */
|
|
3160
|
+
async setCuration(projectId, sessionId, summary) {
|
|
3161
|
+
this.db.prepare(`UPDATE agent_sessions SET summary = ?, curated_at = ? WHERE project_id = ? AND session_id = ?`).run(summary, (/* @__PURE__ */ new Date()).toISOString(), projectId, sessionId);
|
|
3162
|
+
}
|
|
3163
|
+
async recorderTotals(projectId) {
|
|
3164
|
+
const s = this.db.prepare(`SELECT COUNT(*) AS n FROM agent_sessions WHERE project_id = ?`).get(projectId);
|
|
3165
|
+
const e = this.db.prepare(`SELECT COUNT(*) AS n FROM session_events WHERE project_id = ?`).get(projectId);
|
|
3166
|
+
return { sessions: s.n, events: e.n };
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Apply retention (spec §3.7): keep at most `maxSessions` newest sessions per
|
|
3170
|
+
* project (older sessions + their events are dropped), and drop events older
|
|
3171
|
+
* than `retentionDays`. Curated sessions (`curated_at` set — M2) older than 14
|
|
3172
|
+
* days keep only their header+summary (events dropped). Env overrides:
|
|
3173
|
+
* `PROMETHEUS_RECORDER_RETENTION_DAYS` / `PROMETHEUS_RECORDER_MAX_SESSIONS`.
|
|
3174
|
+
*/
|
|
3175
|
+
async applyRecorderRetention(env = process.env) {
|
|
3176
|
+
const days = intFromEnv(env, "PROMETHEUS_RECORDER_RETENTION_DAYS", DEFAULT_RETENTION_DAYS);
|
|
3177
|
+
const maxSessions = intFromEnv(env, "PROMETHEUS_RECORDER_MAX_SESSIONS", DEFAULT_MAX_SESSIONS);
|
|
3178
|
+
const cutoff = new Date(Date.now() - days * 864e5).toISOString();
|
|
3179
|
+
const curatedCutoff = new Date(Date.now() - 14 * 864e5).toISOString();
|
|
3180
|
+
let prunedSessions = 0;
|
|
3181
|
+
let prunedEvents = 0;
|
|
3182
|
+
const tx = this.db.transaction(() => {
|
|
3183
|
+
const overflow = this.db.prepare(`SELECT project_id, session_id FROM (
|
|
3184
|
+
SELECT project_id, session_id,
|
|
3185
|
+
ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY started_at DESC) AS rn
|
|
3186
|
+
FROM agent_sessions
|
|
3187
|
+
) WHERE rn > ?`).all(maxSessions);
|
|
3188
|
+
const delEvents = this.db.prepare(`DELETE FROM session_events WHERE project_id = ? AND session_id = ?`);
|
|
3189
|
+
const delSession = this.db.prepare(`DELETE FROM agent_sessions WHERE project_id = ? AND session_id = ?`);
|
|
3190
|
+
for (const o of overflow) {
|
|
3191
|
+
prunedEvents += delEvents.run(o.project_id, o.session_id).changes;
|
|
3192
|
+
prunedSessions += delSession.run(o.project_id, o.session_id).changes;
|
|
3193
|
+
}
|
|
3194
|
+
prunedEvents += this.db.prepare(`DELETE FROM session_events WHERE ts < ?`).run(cutoff).changes;
|
|
3195
|
+
const curated = this.db.prepare(`SELECT project_id, session_id FROM agent_sessions
|
|
3196
|
+
WHERE curated_at IS NOT NULL AND curated_at < ?`).all(curatedCutoff);
|
|
3197
|
+
for (const c of curated) {
|
|
3198
|
+
prunedEvents += delEvents.run(c.project_id, c.session_id).changes;
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
tx();
|
|
3202
|
+
return { prunedSessions, prunedEvents };
|
|
3203
|
+
}
|
|
2495
3204
|
async close() {
|
|
2496
3205
|
if (this.closed)
|
|
2497
3206
|
return;
|
|
@@ -2510,7 +3219,7 @@ function projectIdFor(workspaceRoot) {
|
|
|
2510
3219
|
return createHash("sha256").update(abs).digest("hex").slice(0, 16);
|
|
2511
3220
|
}
|
|
2512
3221
|
function defaultMemoryDbPath() {
|
|
2513
|
-
return
|
|
3222
|
+
return join7(homedir5(), ".prometheus", "memory.db");
|
|
2514
3223
|
}
|
|
2515
3224
|
function intEnv(env, name, def) {
|
|
2516
3225
|
const raw = env[name];
|
|
@@ -2778,6 +3487,7 @@ function composeFromEnv(opts) {
|
|
|
2778
3487
|
});
|
|
2779
3488
|
return {
|
|
2780
3489
|
backend,
|
|
3490
|
+
recorder: backend,
|
|
2781
3491
|
workspaceRoot,
|
|
2782
3492
|
projectId,
|
|
2783
3493
|
projectName,
|
|
@@ -2799,47 +3509,57 @@ function composeFromEnv(opts) {
|
|
|
2799
3509
|
};
|
|
2800
3510
|
}
|
|
2801
3511
|
|
|
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
3512
|
// dist/project-files.js
|
|
3513
|
+
import { execFileSync } from "node:child_process";
|
|
2839
3514
|
import * as fs from "node:fs/promises";
|
|
2840
3515
|
import * as path from "node:path";
|
|
2841
3516
|
var MEMORIES_DIR = path.join(".prometheus", "memories");
|
|
2842
3517
|
var PROJECT_FILE_SOURCE = "import:project-file";
|
|
3518
|
+
var VALID_TYPES = /* @__PURE__ */ new Set(["semantic", "procedural", "episodic", "working"]);
|
|
3519
|
+
function serializeMemoryFile(f) {
|
|
3520
|
+
const lines = ["---", `type: ${f.type}`];
|
|
3521
|
+
if (f.tags && f.tags.length > 0)
|
|
3522
|
+
lines.push(`tags: [${f.tags.join(", ")}]`);
|
|
3523
|
+
if (f.confidence !== void 0)
|
|
3524
|
+
lines.push(`confidence: ${f.confidence}`);
|
|
3525
|
+
lines.push(`updated: ${f.updated ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
3526
|
+
if (f.source !== void 0 && f.source !== "")
|
|
3527
|
+
lines.push(`source: ${f.source}`);
|
|
3528
|
+
lines.push("---", "", f.value.replace(/\s+$/, ""), "");
|
|
3529
|
+
return lines.join("\n");
|
|
3530
|
+
}
|
|
3531
|
+
function parseMemoryFile(content) {
|
|
3532
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(content);
|
|
3533
|
+
if (m === null)
|
|
3534
|
+
return { type: "semantic", tags: void 0, confidence: void 0, source: void 0, body: content.trim() };
|
|
3535
|
+
const fm = m[1] ?? "";
|
|
3536
|
+
const body = content.slice(m[0].length).replace(/^\r?\n/, "").trim();
|
|
3537
|
+
let type = "semantic";
|
|
3538
|
+
let tags;
|
|
3539
|
+
let confidence;
|
|
3540
|
+
let source;
|
|
3541
|
+
for (const line of fm.split(/\r?\n/)) {
|
|
3542
|
+
const kv = /^([A-Za-z_]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
3543
|
+
if (kv === null)
|
|
3544
|
+
continue;
|
|
3545
|
+
const key = kv[1].toLowerCase();
|
|
3546
|
+
const val = kv[2].trim();
|
|
3547
|
+
if (key === "type" && VALID_TYPES.has(val))
|
|
3548
|
+
type = val;
|
|
3549
|
+
else if (key === "tags") {
|
|
3550
|
+
const inner = val.replace(/^\[|\]$/g, "");
|
|
3551
|
+
const parsed = inner.split(",").map((t) => t.trim().replace(/^["']|["']$/g, "")).filter((t) => t !== "");
|
|
3552
|
+
if (parsed.length > 0)
|
|
3553
|
+
tags = parsed;
|
|
3554
|
+
} else if (key === "confidence") {
|
|
3555
|
+
const n = Number.parseFloat(val);
|
|
3556
|
+
if (Number.isFinite(n) && n >= 0 && n <= 1)
|
|
3557
|
+
confidence = n;
|
|
3558
|
+
} else if (key === "source" && val !== "")
|
|
3559
|
+
source = val;
|
|
3560
|
+
}
|
|
3561
|
+
return { type, tags, confidence, source, body };
|
|
3562
|
+
}
|
|
2843
3563
|
function memoriesDir(workspaceRoot) {
|
|
2844
3564
|
return path.join(workspaceRoot, MEMORIES_DIR);
|
|
2845
3565
|
}
|
|
@@ -2853,13 +3573,25 @@ function keyToFilename(key) {
|
|
|
2853
3573
|
function filenameToKey(filename) {
|
|
2854
3574
|
return filename.replace(/\.md$/i, "");
|
|
2855
3575
|
}
|
|
2856
|
-
async function
|
|
3576
|
+
async function writeMemoryFile(workspaceRoot, f) {
|
|
2857
3577
|
const dir = memoriesDir(workspaceRoot);
|
|
2858
3578
|
await fs.mkdir(dir, { recursive: true });
|
|
2859
|
-
const file = path.join(dir, keyToFilename(key));
|
|
2860
|
-
await fs.writeFile(file,
|
|
3579
|
+
const file = path.join(dir, keyToFilename(f.key));
|
|
3580
|
+
await fs.writeFile(file, serializeMemoryFile(f), "utf-8");
|
|
2861
3581
|
return file;
|
|
2862
3582
|
}
|
|
3583
|
+
function memoriesShared(workspaceRoot) {
|
|
3584
|
+
const probe = path.join(MEMORIES_DIR, ".prom-shared-probe");
|
|
3585
|
+
try {
|
|
3586
|
+
execFileSync("git", ["-C", workspaceRoot, "check-ignore", "-q", probe], {
|
|
3587
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
3588
|
+
timeout: 3e3
|
|
3589
|
+
});
|
|
3590
|
+
return false;
|
|
3591
|
+
} catch (err) {
|
|
3592
|
+
return err.status === 1;
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
2863
3595
|
async function deleteProjectFile(workspaceRoot, key) {
|
|
2864
3596
|
const file = path.join(memoriesDir(workspaceRoot), keyToFilename(key));
|
|
2865
3597
|
try {
|
|
@@ -2893,67 +3625,196 @@ async function listProjectFiles(workspaceRoot) {
|
|
|
2893
3625
|
async function syncProjectFiles(backend, input) {
|
|
2894
3626
|
const files = await listProjectFiles(input.workspaceRoot);
|
|
2895
3627
|
const scopeId = input.scopeId ?? input.projectId;
|
|
3628
|
+
const fileKeys = /* @__PURE__ */ new Set();
|
|
3629
|
+
const skippedKeys = /* @__PURE__ */ new Set();
|
|
3630
|
+
const skipped = [];
|
|
3631
|
+
let synced = 0;
|
|
2896
3632
|
for (const file of files) {
|
|
3633
|
+
const parsed = parseMemoryFile(file.content);
|
|
3634
|
+
if (findSecretPatterns(parsed.body).length > 0) {
|
|
3635
|
+
skipped.push({ key: file.key, reason: "secret-like content" });
|
|
3636
|
+
skippedKeys.add(file.key);
|
|
3637
|
+
continue;
|
|
3638
|
+
}
|
|
3639
|
+
fileKeys.add(file.key);
|
|
2897
3640
|
await backend.write({
|
|
2898
3641
|
projectId: input.projectId,
|
|
2899
3642
|
scope: "project",
|
|
2900
3643
|
scopeId,
|
|
2901
|
-
type:
|
|
3644
|
+
type: parsed.type,
|
|
2902
3645
|
key: file.key,
|
|
2903
|
-
value:
|
|
3646
|
+
value: parsed.body,
|
|
3647
|
+
...parsed.tags !== void 0 ? { tags: parsed.tags } : {},
|
|
3648
|
+
...parsed.confidence !== void 0 ? { confidence: parsed.confidence } : {},
|
|
2904
3649
|
source: PROJECT_FILE_SOURCE
|
|
2905
3650
|
});
|
|
3651
|
+
synced++;
|
|
2906
3652
|
}
|
|
2907
|
-
|
|
3653
|
+
let pruned = 0;
|
|
3654
|
+
const existing = await backend.list({ projectId: input.projectId, scope: "project" });
|
|
3655
|
+
for (const rec of existing) {
|
|
3656
|
+
if (rec.source !== PROJECT_FILE_SOURCE)
|
|
3657
|
+
continue;
|
|
3658
|
+
if (fileKeys.has(rec.key) || skippedKeys.has(rec.key))
|
|
3659
|
+
continue;
|
|
3660
|
+
await backend.delete({
|
|
3661
|
+
projectId: input.projectId,
|
|
3662
|
+
scope: "project",
|
|
3663
|
+
scopeId,
|
|
3664
|
+
type: rec.type,
|
|
3665
|
+
key: rec.key
|
|
3666
|
+
});
|
|
3667
|
+
pruned++;
|
|
3668
|
+
}
|
|
3669
|
+
return { synced, pruned, skipped };
|
|
2908
3670
|
}
|
|
2909
3671
|
|
|
2910
|
-
// dist/
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
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
|
|
3672
|
+
// dist/curation.js
|
|
3673
|
+
function sessionLog(events) {
|
|
3674
|
+
const lines = [];
|
|
3675
|
+
for (const e of events) {
|
|
3676
|
+
if (e.eventType === "user_message")
|
|
3677
|
+
lines.push(`USER: ${e.content}`);
|
|
3678
|
+
else if (e.eventType === "assistant_message")
|
|
3679
|
+
lines.push(`ASSISTANT: ${e.content}`);
|
|
3680
|
+
else if (e.eventType === "tool_use")
|
|
3681
|
+
lines.push(e.content);
|
|
2935
3682
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
3683
|
+
return lines.join("\n");
|
|
3684
|
+
}
|
|
3685
|
+
async function curateSession(deps, sessionId) {
|
|
3686
|
+
const { backend, recorder, extractor, projectId, workspaceRoot, mirrorToFiles } = deps;
|
|
3687
|
+
const detail = await recorder.getSession(projectId, sessionId, 4e4);
|
|
3688
|
+
if (detail === null)
|
|
3689
|
+
return { curated: false, reason: "no such session" };
|
|
3690
|
+
const log = sessionLog(detail.events);
|
|
3691
|
+
if (log.trim() === "") {
|
|
3692
|
+
await recorder.setCuration(projectId, sessionId, "");
|
|
3693
|
+
return { curated: true, summary: "", facts: 0, procedures: 0 };
|
|
2942
3694
|
}
|
|
2943
|
-
|
|
3695
|
+
let result;
|
|
3696
|
+
try {
|
|
3697
|
+
result = await extractor.curate(log);
|
|
3698
|
+
} catch {
|
|
3699
|
+
return { curated: false, reason: "curation call failed" };
|
|
3700
|
+
}
|
|
3701
|
+
if (result.summary.trim() === "") {
|
|
3702
|
+
return { curated: false, reason: "curator returned no summary (provider unavailable?)" };
|
|
3703
|
+
}
|
|
3704
|
+
const scope = "project";
|
|
3705
|
+
const scopeId = scopeIdFor(scope, projectId);
|
|
3706
|
+
const source = `curated:session:${sessionId}`;
|
|
3707
|
+
const summary = findSecretPatterns(result.summary).length > 0 ? "[dropped: secret-like]" : result.summary;
|
|
3708
|
+
await recorder.setCuration(projectId, sessionId, summary);
|
|
3709
|
+
await backend.write({
|
|
3710
|
+
projectId,
|
|
3711
|
+
scope,
|
|
3712
|
+
scopeId,
|
|
3713
|
+
type: "episodic",
|
|
3714
|
+
key: `session:${sessionId}`,
|
|
3715
|
+
value: summary,
|
|
3716
|
+
source
|
|
3717
|
+
});
|
|
3718
|
+
let factCount = 0;
|
|
3719
|
+
for (const f of result.facts) {
|
|
3720
|
+
try {
|
|
3721
|
+
assertNoSecrets(`${f.key}
|
|
3722
|
+
${f.value}`);
|
|
3723
|
+
} catch {
|
|
3724
|
+
continue;
|
|
3725
|
+
}
|
|
3726
|
+
await backend.write({
|
|
3727
|
+
projectId,
|
|
3728
|
+
scope,
|
|
3729
|
+
scopeId,
|
|
3730
|
+
type: "semantic",
|
|
3731
|
+
key: f.key,
|
|
3732
|
+
value: f.value,
|
|
3733
|
+
...f.confidence !== void 0 ? { confidence: f.confidence } : {},
|
|
3734
|
+
source
|
|
3735
|
+
});
|
|
3736
|
+
if (mirrorToFiles) {
|
|
3737
|
+
try {
|
|
3738
|
+
await writeMemoryFile(workspaceRoot, {
|
|
3739
|
+
key: f.key,
|
|
3740
|
+
type: "semantic",
|
|
3741
|
+
value: f.value,
|
|
3742
|
+
...f.confidence !== void 0 ? { confidence: f.confidence } : {},
|
|
3743
|
+
source
|
|
3744
|
+
});
|
|
3745
|
+
} catch {
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
factCount++;
|
|
3749
|
+
}
|
|
3750
|
+
let procCount = 0;
|
|
3751
|
+
for (const p of result.procedures) {
|
|
3752
|
+
try {
|
|
3753
|
+
assertNoSecrets(`${p.key}
|
|
3754
|
+
${p.value}`);
|
|
3755
|
+
} catch {
|
|
3756
|
+
continue;
|
|
3757
|
+
}
|
|
3758
|
+
await backend.write({
|
|
3759
|
+
projectId,
|
|
3760
|
+
scope,
|
|
3761
|
+
scopeId,
|
|
3762
|
+
type: "procedural",
|
|
3763
|
+
key: p.key,
|
|
3764
|
+
value: p.value,
|
|
3765
|
+
source
|
|
3766
|
+
});
|
|
3767
|
+
if (mirrorToFiles) {
|
|
3768
|
+
try {
|
|
3769
|
+
await writeMemoryFile(workspaceRoot, { key: p.key, type: "procedural", value: p.value, source });
|
|
3770
|
+
} catch {
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
procCount++;
|
|
3774
|
+
}
|
|
3775
|
+
return { curated: true, summary, facts: factCount, procedures: procCount };
|
|
2944
3776
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3777
|
+
|
|
3778
|
+
// dist/roots.js
|
|
3779
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3780
|
+
async function rootFromClient(server, timeoutMs = 2500) {
|
|
3781
|
+
let supportsRoots = false;
|
|
3782
|
+
try {
|
|
3783
|
+
supportsRoots = server.getClientCapabilities()?.roots != null;
|
|
3784
|
+
} catch {
|
|
3785
|
+
return null;
|
|
3786
|
+
}
|
|
3787
|
+
if (!supportsRoots)
|
|
3788
|
+
return null;
|
|
3789
|
+
let res;
|
|
3790
|
+
try {
|
|
3791
|
+
res = await server.listRoots(void 0, { timeout: timeoutMs });
|
|
3792
|
+
} catch {
|
|
3793
|
+
return null;
|
|
3794
|
+
}
|
|
3795
|
+
const roots = res?.roots ?? [];
|
|
3796
|
+
for (const r of roots) {
|
|
3797
|
+
const uri = typeof r?.uri === "string" ? r.uri : "";
|
|
3798
|
+
if (uri.startsWith("file://")) {
|
|
3799
|
+
try {
|
|
3800
|
+
return fileURLToPath2(uri);
|
|
3801
|
+
} catch {
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
2950
3804
|
}
|
|
3805
|
+
return null;
|
|
2951
3806
|
}
|
|
2952
3807
|
|
|
3808
|
+
// dist/server.js
|
|
3809
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3810
|
+
|
|
3811
|
+
// dist/tools.js
|
|
3812
|
+
import { z } from "zod";
|
|
3813
|
+
|
|
2953
3814
|
// dist/setup.js
|
|
2954
|
-
import { existsSync, readFileSync as
|
|
3815
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
2955
3816
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
2956
|
-
import { dirname as
|
|
3817
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
2957
3818
|
var MEMORY_RUNTIMES = [
|
|
2958
3819
|
"claude-code",
|
|
2959
3820
|
"cursor",
|
|
@@ -2997,13 +3858,13 @@ alwaysApply: true
|
|
|
2997
3858
|
var TARGETS = {
|
|
2998
3859
|
"claude-code": { relPath: "CLAUDE.md", mode: "block", detect: "CLAUDE.md" },
|
|
2999
3860
|
cursor: {
|
|
3000
|
-
relPath:
|
|
3861
|
+
relPath: join9(".cursor", "rules", "prometheus-memory.mdc"),
|
|
3001
3862
|
mode: "file",
|
|
3002
3863
|
fileContent: CURSOR_FRONTMATTER + withMarkers(RULE_BLOCK) + "\n",
|
|
3003
3864
|
detect: ".cursor"
|
|
3004
3865
|
},
|
|
3005
3866
|
augment: {
|
|
3006
|
-
relPath:
|
|
3867
|
+
relPath: join9(".augment", "rules", "prometheus-memory.md"),
|
|
3007
3868
|
mode: "file",
|
|
3008
3869
|
fileContent: withMarkers(RULE_BLOCK) + "\n",
|
|
3009
3870
|
detect: ".augment"
|
|
@@ -3011,19 +3872,19 @@ var TARGETS = {
|
|
|
3011
3872
|
agents: { relPath: "AGENTS.md", mode: "block", detect: "AGENTS.md" }
|
|
3012
3873
|
};
|
|
3013
3874
|
function detectRuntimes(workspaceRoot) {
|
|
3014
|
-
const found = MEMORY_RUNTIMES.filter((rt) =>
|
|
3875
|
+
const found = MEMORY_RUNTIMES.filter((rt) => existsSync3(join9(workspaceRoot, TARGETS[rt].detect)));
|
|
3015
3876
|
return found.length > 0 ? found : ["agents"];
|
|
3016
3877
|
}
|
|
3017
3878
|
function existingRuntimes(workspaceRoot) {
|
|
3018
|
-
return MEMORY_RUNTIMES.filter((rt) =>
|
|
3879
|
+
return MEMORY_RUNTIMES.filter((rt) => existsSync3(join9(workspaceRoot, TARGETS[rt].detect)));
|
|
3019
3880
|
}
|
|
3020
3881
|
function installedRuntimes(workspaceRoot) {
|
|
3021
3882
|
return MEMORY_RUNTIMES.filter((rt) => {
|
|
3022
|
-
const p =
|
|
3023
|
-
if (!
|
|
3883
|
+
const p = join9(workspaceRoot, TARGETS[rt].relPath);
|
|
3884
|
+
if (!existsSync3(p))
|
|
3024
3885
|
return false;
|
|
3025
3886
|
try {
|
|
3026
|
-
return
|
|
3887
|
+
return readFileSync4(p, "utf-8").includes(BLOCK_START);
|
|
3027
3888
|
} catch {
|
|
3028
3889
|
return false;
|
|
3029
3890
|
}
|
|
@@ -3048,14 +3909,14 @@ function upsertBlock(existing, block) {
|
|
|
3048
3909
|
}
|
|
3049
3910
|
async function installRuntime(workspaceRoot, runtime) {
|
|
3050
3911
|
const target = TARGETS[runtime];
|
|
3051
|
-
const absPath =
|
|
3052
|
-
const exists =
|
|
3912
|
+
const absPath = join9(workspaceRoot, target.relPath);
|
|
3913
|
+
const exists = existsSync3(absPath);
|
|
3053
3914
|
const before = exists ? await readFile3(absPath, "utf-8") : "";
|
|
3054
3915
|
const after = target.mode === "file" ? target.fileContent : upsertBlock(before, RULE_BLOCK);
|
|
3055
3916
|
if (exists && before === after) {
|
|
3056
3917
|
return { runtime, path: absPath, action: "unchanged" };
|
|
3057
3918
|
}
|
|
3058
|
-
await mkdir3(
|
|
3919
|
+
await mkdir3(dirname5(absPath), { recursive: true });
|
|
3059
3920
|
await writeFile3(absPath, after, "utf-8");
|
|
3060
3921
|
return { runtime, path: absPath, action: exists ? "updated" : "created" };
|
|
3061
3922
|
}
|
|
@@ -3143,6 +4004,9 @@ function recordToJson(rec) {
|
|
|
3143
4004
|
value: rec.value,
|
|
3144
4005
|
confidence: rec.confidence ?? null,
|
|
3145
4006
|
source: rec.source ?? null,
|
|
4007
|
+
// M3-git: where this record came from — a committed/shared file, or the
|
|
4008
|
+
// local DB only (a private write or a not-yet-shared record).
|
|
4009
|
+
origin: rec.source === PROJECT_FILE_SOURCE ? "file" : "local",
|
|
3146
4010
|
tags: rec.tags ?? [],
|
|
3147
4011
|
useCount: rec.useCount,
|
|
3148
4012
|
createdAt: rec.createdAt,
|
|
@@ -3174,7 +4038,13 @@ var writeInput = {
|
|
|
3174
4038
|
key: z.string().min(1, "key must not be empty"),
|
|
3175
4039
|
value: z.string().min(1, "value must not be empty"),
|
|
3176
4040
|
confidence: z.number().min(0).max(1).optional(),
|
|
3177
|
-
tags: z.array(z.string().min(1)).optional()
|
|
4041
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
4042
|
+
/**
|
|
4043
|
+
* M3-git: promote this record to a committed `.prometheus/memories/` file so
|
|
4044
|
+
* it is SHARED with the team via git (file = shared, DB = private). Default
|
|
4045
|
+
* false (DB-only). Only project-scope semantic/procedural records are shareable.
|
|
4046
|
+
*/
|
|
4047
|
+
share: z.boolean().optional()
|
|
3178
4048
|
};
|
|
3179
4049
|
var captureInput = {
|
|
3180
4050
|
sessionId: z.string().min(1, "sessionId must not be empty"),
|
|
@@ -3207,12 +4077,31 @@ var deleteInput = {
|
|
|
3207
4077
|
var searchInput = {
|
|
3208
4078
|
query: z.string().min(1, "query must not be empty"),
|
|
3209
4079
|
types: z.array(typeEnum).min(1).optional(),
|
|
3210
|
-
limit: z.number().int().positive().max(MAX_LIMIT).optional()
|
|
4080
|
+
limit: z.number().int().positive().max(MAX_LIMIT).optional(),
|
|
4081
|
+
/**
|
|
4082
|
+
* Which stores to search. Default `["memories"]` (unchanged behaviour).
|
|
4083
|
+
* `"sessions"` also searches the Session Recorder's event log (M1).
|
|
4084
|
+
*/
|
|
4085
|
+
sources: z.array(z.enum(["memories", "sessions"])).min(1).optional()
|
|
3211
4086
|
};
|
|
3212
4087
|
var runtimeEnum = z.enum(MEMORY_RUNTIMES);
|
|
3213
4088
|
var setupInput = {
|
|
3214
4089
|
runtimes: z.array(runtimeEnum).min(1).optional()
|
|
3215
4090
|
};
|
|
4091
|
+
var recorderSetupInput = {
|
|
4092
|
+
scope: z.enum(["user", "project", "project-local"]).optional(),
|
|
4093
|
+
uninstall: z.boolean().optional()
|
|
4094
|
+
};
|
|
4095
|
+
var sessionsInput = {
|
|
4096
|
+
mode: z.enum(["list", "get"]).optional(),
|
|
4097
|
+
sessionId: z.string().min(1).optional(),
|
|
4098
|
+
limit: z.number().int().positive().max(MAX_LIMIT).optional(),
|
|
4099
|
+
maxChars: z.number().int().positive().optional()
|
|
4100
|
+
};
|
|
4101
|
+
var curateInput = {
|
|
4102
|
+
sessionId: z.string().min(1).optional(),
|
|
4103
|
+
limit: z.number().int().positive().max(MAX_LIMIT).optional()
|
|
4104
|
+
};
|
|
3216
4105
|
var emptyInput = {};
|
|
3217
4106
|
function registerTools(server, source, hooks = {}) {
|
|
3218
4107
|
const ready = typeof source === "function" ? source : () => Promise.resolve(source);
|
|
@@ -3235,7 +4124,7 @@ function registerTools(server, source, hooks = {}) {
|
|
|
3235
4124
|
const { backend, workspaceRoot, projectId, projectName } = deps;
|
|
3236
4125
|
const mirrorToFiles = !deps.rootIsHomeOrFsRoot;
|
|
3237
4126
|
const limit = clampLimit(args.limit, DEFAULT_READ_LIMIT);
|
|
3238
|
-
const
|
|
4127
|
+
const sync = mirrorToFiles ? await syncProjectFiles(backend, { projectId, workspaceRoot }) : { synced: 0, pruned: 0, skipped: [] };
|
|
3239
4128
|
const records = await backend.read({
|
|
3240
4129
|
chain: defaultScopeChain(projectId),
|
|
3241
4130
|
types: args.types,
|
|
@@ -3244,7 +4133,9 @@ function registerTools(server, source, hooks = {}) {
|
|
|
3244
4133
|
return textResult({
|
|
3245
4134
|
projectId,
|
|
3246
4135
|
projectName,
|
|
3247
|
-
projectFilesSynced: synced,
|
|
4136
|
+
projectFilesSynced: sync.synced,
|
|
4137
|
+
projectFilesPruned: sync.pruned,
|
|
4138
|
+
...sync.skipped.length > 0 ? { skippedFiles: sync.skipped } : {},
|
|
3248
4139
|
woven: weave(records),
|
|
3249
4140
|
records: records.map(recordToJson)
|
|
3250
4141
|
});
|
|
@@ -3274,10 +4165,18 @@ ${args.value}`);
|
|
|
3274
4165
|
source: "user"
|
|
3275
4166
|
});
|
|
3276
4167
|
let projectFile = null;
|
|
3277
|
-
|
|
3278
|
-
|
|
4168
|
+
const shareable = scope === "project" && (args.type === "semantic" || args.type === "procedural");
|
|
4169
|
+
if (mirrorToFiles && shareable && args.share === true) {
|
|
4170
|
+
projectFile = await writeMemoryFile(workspaceRoot, {
|
|
4171
|
+
key: args.key,
|
|
4172
|
+
type: args.type,
|
|
4173
|
+
value: args.value,
|
|
4174
|
+
...args.tags !== void 0 ? { tags: args.tags } : {},
|
|
4175
|
+
...args.confidence !== void 0 ? { confidence: args.confidence } : {},
|
|
4176
|
+
source: "shared:user"
|
|
4177
|
+
});
|
|
3279
4178
|
}
|
|
3280
|
-
return textResult({ record: recordToJson(record), projectFile });
|
|
4179
|
+
return textResult({ record: recordToJson(record), projectFile, shared: projectFile !== null });
|
|
3281
4180
|
});
|
|
3282
4181
|
reg("capture", {
|
|
3283
4182
|
title: "Consolidate session learnings",
|
|
@@ -3349,24 +4248,31 @@ ${f.value}`);
|
|
|
3349
4248
|
inputSchema: searchInput
|
|
3350
4249
|
}, async (args) => {
|
|
3351
4250
|
const deps = await ready();
|
|
3352
|
-
const { backend, workspaceRoot, projectId } = deps;
|
|
4251
|
+
const { backend, recorder, workspaceRoot, projectId } = deps;
|
|
3353
4252
|
const mirrorToFiles = !deps.rootIsHomeOrFsRoot;
|
|
3354
4253
|
const limit = clampLimit(args.limit, 20);
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
4254
|
+
const sources = args.sources ?? ["memories"];
|
|
4255
|
+
let hits = [];
|
|
4256
|
+
if (sources.includes("memories")) {
|
|
4257
|
+
if (mirrorToFiles)
|
|
4258
|
+
await syncProjectFiles(backend, { projectId, workspaceRoot });
|
|
4259
|
+
hits = await backend.search({
|
|
4260
|
+
chain: defaultScopeChain(projectId),
|
|
4261
|
+
query: args.query,
|
|
4262
|
+
types: args.types,
|
|
4263
|
+
limit
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
4266
|
+
let sessionHits = [];
|
|
4267
|
+
if (sources.includes("sessions")) {
|
|
4268
|
+
sessionHits = await recorder.searchSessions(projectId, args.query, limit);
|
|
4269
|
+
}
|
|
3363
4270
|
return textResult({
|
|
3364
4271
|
projectId,
|
|
3365
4272
|
query: args.query,
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
}))
|
|
4273
|
+
sources,
|
|
4274
|
+
hits: hits.map((h) => ({ snippet: h.snippet, record: recordToJson(h.record) })),
|
|
4275
|
+
sessions: sessionHits
|
|
3370
4276
|
});
|
|
3371
4277
|
});
|
|
3372
4278
|
reg("list", {
|
|
@@ -3407,7 +4313,7 @@ ${f.value}`);
|
|
|
3407
4313
|
key: args.key
|
|
3408
4314
|
});
|
|
3409
4315
|
let fileRemoved = false;
|
|
3410
|
-
if (mirrorToFiles && scope === "project" && args.type === "semantic") {
|
|
4316
|
+
if (mirrorToFiles && scope === "project" && (args.type === "semantic" || args.type === "procedural")) {
|
|
3411
4317
|
fileRemoved = await deleteProjectFile(workspaceRoot, args.key);
|
|
3412
4318
|
}
|
|
3413
4319
|
return textResult({ removed, fileRemoved });
|
|
@@ -3434,6 +4340,82 @@ ${f.value}`);
|
|
|
3434
4340
|
}
|
|
3435
4341
|
return textResult({ workspaceRoot, results });
|
|
3436
4342
|
});
|
|
4343
|
+
reg("recorder_setup", {
|
|
4344
|
+
title: "Install the Session Recorder (opt-in)",
|
|
4345
|
+
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).",
|
|
4346
|
+
inputSchema: recorderSetupInput
|
|
4347
|
+
}, async (args) => {
|
|
4348
|
+
const deps = await ready();
|
|
4349
|
+
try {
|
|
4350
|
+
const result = applyRecorderHooks({
|
|
4351
|
+
scope: args.scope ?? "project-local",
|
|
4352
|
+
projectRoot: deps.workspaceRoot,
|
|
4353
|
+
uninstall: args.uninstall === true
|
|
4354
|
+
});
|
|
4355
|
+
return textResult({ ok: true, ...result });
|
|
4356
|
+
} catch (err) {
|
|
4357
|
+
return textResult({
|
|
4358
|
+
ok: false,
|
|
4359
|
+
reason: `recorder_setup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4360
|
+
});
|
|
4361
|
+
}
|
|
4362
|
+
});
|
|
4363
|
+
reg("sessions", {
|
|
4364
|
+
title: "Browse recorded coding sessions",
|
|
4365
|
+
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.",
|
|
4366
|
+
inputSchema: sessionsInput
|
|
4367
|
+
}, async (args) => {
|
|
4368
|
+
const { recorder, projectId } = await ready();
|
|
4369
|
+
const mode = args.mode ?? "list";
|
|
4370
|
+
if (mode === "get") {
|
|
4371
|
+
const sessionId = (args.sessionId ?? "").trim();
|
|
4372
|
+
if (sessionId === "") {
|
|
4373
|
+
return textResult({ ok: false, reason: "mode 'get' requires a sessionId." });
|
|
4374
|
+
}
|
|
4375
|
+
const detail = await recorder.getSession(projectId, sessionId, args.maxChars ?? void 0);
|
|
4376
|
+
if (detail === null) {
|
|
4377
|
+
return textResult({ ok: false, reason: `no recorded session "${sessionId}" for this project.` });
|
|
4378
|
+
}
|
|
4379
|
+
return textResult({ ok: true, ...detail });
|
|
4380
|
+
}
|
|
4381
|
+
const limit = clampLimit(args.limit, 20);
|
|
4382
|
+
const sessions = await recorder.listSessions(projectId, limit);
|
|
4383
|
+
return textResult({ ok: true, projectId, count: sessions.length, sessions });
|
|
4384
|
+
});
|
|
4385
|
+
reg("curate", {
|
|
4386
|
+
title: "Distil recorded sessions into durable memory",
|
|
4387
|
+
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`).",
|
|
4388
|
+
inputSchema: curateInput
|
|
4389
|
+
}, async (args) => {
|
|
4390
|
+
const deps = await ready();
|
|
4391
|
+
const { extractor, recorder, backend, projectId, workspaceRoot } = deps;
|
|
4392
|
+
if (extractor === null) {
|
|
4393
|
+
return textResult({
|
|
4394
|
+
ok: false,
|
|
4395
|
+
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."
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
const curateDeps = {
|
|
4399
|
+
backend,
|
|
4400
|
+
recorder,
|
|
4401
|
+
extractor,
|
|
4402
|
+
projectId,
|
|
4403
|
+
workspaceRoot,
|
|
4404
|
+
mirrorToFiles: !deps.rootIsHomeOrFsRoot
|
|
4405
|
+
};
|
|
4406
|
+
const sessionId = (args.sessionId ?? "").trim();
|
|
4407
|
+
if (sessionId !== "") {
|
|
4408
|
+
const outcome = await curateSession(curateDeps, sessionId);
|
|
4409
|
+
return textResult({ ok: outcome.curated, ...outcome });
|
|
4410
|
+
}
|
|
4411
|
+
const limit = clampLimit(args.limit, 10);
|
|
4412
|
+
const pending = await recorder.listUncuratedSessions(projectId, limit);
|
|
4413
|
+
const results = [];
|
|
4414
|
+
for (const s of pending) {
|
|
4415
|
+
results.push({ sessionId: s.sessionId, ...await curateSession(curateDeps, s.sessionId) });
|
|
4416
|
+
}
|
|
4417
|
+
return textResult({ ok: true, curatedCount: results.filter((r) => r.curated).length, results });
|
|
4418
|
+
});
|
|
3437
4419
|
reg("status", {
|
|
3438
4420
|
title: "Memory status / health check",
|
|
3439
4421
|
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 +4437,36 @@ ${f.value}`);
|
|
|
3455
4437
|
embeddingsError = err instanceof Error ? err.message : String(err);
|
|
3456
4438
|
}
|
|
3457
4439
|
}
|
|
3458
|
-
const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.
|
|
4440
|
+
const update = await buildUpdateStatus("@prom.codes/memory-mcp", "0.15.0", { isDevBuild: false });
|
|
4441
|
+
let recorder;
|
|
4442
|
+
try {
|
|
4443
|
+
const scopes = ["project-local", "project", "user"];
|
|
4444
|
+
let rec = recorderStatus({ scope: "project-local", projectRoot: workspaceRoot });
|
|
4445
|
+
for (const s of scopes) {
|
|
4446
|
+
const st = recorderStatus({ scope: s, projectRoot: workspaceRoot });
|
|
4447
|
+
if (st.installed) {
|
|
4448
|
+
rec = st;
|
|
4449
|
+
break;
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
const totals = await deps.recorder.recorderTotals(projectId);
|
|
4453
|
+
recorder = {
|
|
4454
|
+
installed: rec.installed,
|
|
4455
|
+
scope: rec.scope,
|
|
4456
|
+
events: rec.events,
|
|
4457
|
+
settingsPath: rec.settingsPath,
|
|
4458
|
+
hookScriptPresent: rec.hookScriptPresent,
|
|
4459
|
+
sessions: totals.sessions,
|
|
4460
|
+
totalEvents: totals.events,
|
|
4461
|
+
retention: {
|
|
4462
|
+
days: Number(process.env.PROMETHEUS_RECORDER_RETENTION_DAYS ?? 90),
|
|
4463
|
+
maxSessions: Number(process.env.PROMETHEUS_RECORDER_MAX_SESSIONS ?? 300)
|
|
4464
|
+
},
|
|
4465
|
+
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)."
|
|
4466
|
+
};
|
|
4467
|
+
} catch {
|
|
4468
|
+
recorder = { installed: false, error: "recorder status unavailable" };
|
|
4469
|
+
}
|
|
3459
4470
|
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
4471
|
return textResult({
|
|
3461
4472
|
installed: true,
|
|
@@ -3468,6 +4479,22 @@ ${f.value}`);
|
|
|
3468
4479
|
autoSetup: deps.autoSetup
|
|
3469
4480
|
},
|
|
3470
4481
|
storage: { dbPath, projectFileMirror: mirrorToFiles },
|
|
4482
|
+
// M3-git L1 boot-check: is `.prometheus/memories/` committed (shared) or
|
|
4483
|
+
// gitignored (private/off)? Best-effort; never throws.
|
|
4484
|
+
sharedMemory: (() => {
|
|
4485
|
+
if (deps.rootIsHomeOrFsRoot)
|
|
4486
|
+
return { shared: false, note: "no project open" };
|
|
4487
|
+
let shared = false;
|
|
4488
|
+
try {
|
|
4489
|
+
shared = memoriesShared(workspaceRoot);
|
|
4490
|
+
} catch {
|
|
4491
|
+
shared = false;
|
|
4492
|
+
}
|
|
4493
|
+
return {
|
|
4494
|
+
shared,
|
|
4495
|
+
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."
|
|
4496
|
+
};
|
|
4497
|
+
})(),
|
|
3471
4498
|
records: { total: stats.total, byScope: stats.byScope },
|
|
3472
4499
|
embeddings: {
|
|
3473
4500
|
enabled: deps.embeddingsEnabled,
|
|
@@ -3482,6 +4509,7 @@ ${f.value}`);
|
|
|
3482
4509
|
dedup: deps.dedupEnabled,
|
|
3483
4510
|
extract: deps.extractorId
|
|
3484
4511
|
},
|
|
4512
|
+
recorder,
|
|
3485
4513
|
update,
|
|
3486
4514
|
summary
|
|
3487
4515
|
});
|
|
@@ -3491,7 +4519,7 @@ ${f.value}`);
|
|
|
3491
4519
|
// dist/server.js
|
|
3492
4520
|
var SERVER_IDENTITY = {
|
|
3493
4521
|
name: "prometheus-memory-mcp",
|
|
3494
|
-
version: "0.
|
|
4522
|
+
version: "0.15.0",
|
|
3495
4523
|
title: "prom.codes Memory"
|
|
3496
4524
|
};
|
|
3497
4525
|
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 +4554,8 @@ async function main() {
|
|
|
3526
4554
|
onToolCall: (tool) => heartbeat.update({ lastTool: tool, lastToolCallAt: Date.now() })
|
|
3527
4555
|
});
|
|
3528
4556
|
let watchdog = null;
|
|
4557
|
+
let recorderTimer = null;
|
|
4558
|
+
const RECORDER_INGEST_MS = 6e4;
|
|
3529
4559
|
let shuttingDown = false;
|
|
3530
4560
|
const shutdown = async (reason) => {
|
|
3531
4561
|
if (shuttingDown)
|
|
@@ -3534,6 +4564,8 @@ async function main() {
|
|
|
3534
4564
|
process.stderr.write(`prometheus-memory-mcp: ${reason}, shutting down
|
|
3535
4565
|
`);
|
|
3536
4566
|
watchdog?.stop();
|
|
4567
|
+
if (recorderTimer !== null)
|
|
4568
|
+
clearInterval(recorderTimer);
|
|
3537
4569
|
heartbeat.stop();
|
|
3538
4570
|
try {
|
|
3539
4571
|
await server.close();
|
|
@@ -3590,6 +4622,38 @@ async function main() {
|
|
|
3590
4622
|
});
|
|
3591
4623
|
}
|
|
3592
4624
|
composedResolve(composed);
|
|
4625
|
+
const ingestOnce = async () => {
|
|
4626
|
+
try {
|
|
4627
|
+
const r = await composed.recorder.ingestSpoolDir(composed.projectId, env);
|
|
4628
|
+
if (r.events > 0) {
|
|
4629
|
+
process.stderr.write(`prometheus-memory-mcp: recorder ingested ${r.events} event(s) from ${r.sessions} session(s); reclaimed ${r.deletedSpools} spool(s)
|
|
4630
|
+
`);
|
|
4631
|
+
}
|
|
4632
|
+
const c = composed;
|
|
4633
|
+
if (c.extractor !== null) {
|
|
4634
|
+
const pending = await c.recorder.listUncuratedSessions(c.projectId, 3);
|
|
4635
|
+
for (const s of pending) {
|
|
4636
|
+
const outcome = await curateSession({
|
|
4637
|
+
backend: c.backend,
|
|
4638
|
+
recorder: c.recorder,
|
|
4639
|
+
extractor: c.extractor,
|
|
4640
|
+
projectId: c.projectId,
|
|
4641
|
+
workspaceRoot: c.workspaceRoot,
|
|
4642
|
+
mirrorToFiles: !c.rootIsHomeOrFsRoot
|
|
4643
|
+
}, s.sessionId);
|
|
4644
|
+
if (outcome.curated) {
|
|
4645
|
+
process.stderr.write(`prometheus-memory-mcp: curated session ${s.sessionId} (${outcome.facts ?? 0} facts, ${outcome.procedures ?? 0} procedures)
|
|
4646
|
+
`);
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
await composed.recorder.applyRecorderRetention(env);
|
|
4651
|
+
} catch {
|
|
4652
|
+
}
|
|
4653
|
+
};
|
|
4654
|
+
void ingestOnce();
|
|
4655
|
+
recorderTimer = setInterval(() => void ingestOnce(), RECORDER_INGEST_MS);
|
|
4656
|
+
recorderTimer.unref?.();
|
|
3593
4657
|
};
|
|
3594
4658
|
if (eagerVia !== null) {
|
|
3595
4659
|
boot(void 0, eagerVia);
|