sessionmem 1.0.6 → 1.1.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/adapters/capabilities/fallbackTools.js +2 -2
- package/dist/adapters/claudeMdInjector.js +49 -5
- package/dist/adapters/factory.js +68 -9
- package/dist/adapters/generic.js +147 -12
- package/dist/adapters/global/antigravity.js +14 -7
- package/dist/adapters/global/claudeCode.js +46 -10
- package/dist/adapters/global/codex.js +73 -13
- package/dist/adapters/global/qcoder.js +18 -5
- package/dist/adapters/ide/cline.js +54 -9
- package/dist/adapters/ide/cursor.js +15 -13
- package/dist/adapters/ide/installer.js +201 -8
- package/dist/adapters/ide/windsurf.js +14 -13
- package/dist/cli/commands/config.js +10 -1
- package/dist/cli/commands/import.js +6 -1
- package/dist/cli/commands/install.js +57 -16
- package/dist/cli/commands/ping.js +42 -8
- package/dist/cli/commands/reEmbed.js +4 -3
- package/dist/cli/commands/run.js +7 -17
- package/dist/cli/commands/savings.js +33 -17
- package/dist/cli/commands/sessionEnd.js +124 -0
- package/dist/cli/commands/sessionStart.js +52 -0
- package/dist/cli/commands/sync.js +39 -9
- package/dist/cli/commands/uninstall.js +35 -9
- package/dist/cli/context.js +14 -18
- package/dist/cli/index.js +16 -4
- package/dist/cli/projectId.js +69 -0
- package/dist/core/api/contracts.js +155 -42
- package/dist/core/api/errors.js +4 -7
- package/dist/core/api/memoryCoreService.js +319 -252
- package/dist/core/api/sessionLifecycleService.js +8 -0
- package/dist/core/config/policyConfig.js +33 -6
- package/dist/core/injection/formatStartupInjection.js +53 -9
- package/dist/core/retrieve/recencyBands.js +4 -1
- package/dist/core/retrieve/retrieveMemories.js +10 -8
- package/dist/core/schema/migrations/005_team_provenance.sql +5 -0
- package/dist/core/schema/migrations/006_access_pattern_boosting.sql +5 -0
- package/dist/core/schema/migrations/008_fts5_search.sql +6 -2
- package/dist/core/schema/migrations/009_session_events_unique.sql +24 -0
- package/dist/core/schema/runMigrations.js +64 -2
- package/dist/core/storage/memoryRepo.js +164 -7
- package/dist/core/storage/memorySearchRepo.js +45 -7
- package/dist/core/storage/sessionEventsRepo.js +15 -2
- package/dist/core/summarize/cloudSummarizer.js +15 -2
- package/dist/core/summarize/redaction.js +45 -8
- package/package.json +2 -2
|
@@ -16,7 +16,7 @@ function getSearchStatements(db) {
|
|
|
16
16
|
WHERE project_id = ?
|
|
17
17
|
AND (
|
|
18
18
|
importance >= 8
|
|
19
|
-
OR updated_at >
|
|
19
|
+
OR updated_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-90 days')
|
|
20
20
|
)
|
|
21
21
|
`),
|
|
22
22
|
searchCandidatesFTS: db.prepare(`
|
|
@@ -51,6 +51,17 @@ function parseEmbedding(value) {
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
function dedupById(candidates) {
|
|
55
|
+
// Defensive — FTS should not emit duplicates, but this guards backfill
|
|
56
|
+
// corruption (e.g. a double-run 008 migration) from inflating results.
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
return candidates.filter((candidate) => {
|
|
59
|
+
if (seen.has(candidate.id))
|
|
60
|
+
return false;
|
|
61
|
+
seen.add(candidate.id);
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
54
65
|
function mapRows(rows) {
|
|
55
66
|
return rows.map((row) => {
|
|
56
67
|
const parsed = parseEmbedding(row.embedding);
|
|
@@ -80,9 +91,19 @@ function sanitizeFtsQuery(queryText) {
|
|
|
80
91
|
}
|
|
81
92
|
/**
|
|
82
93
|
* Pre-filter candidates using FTS5 full-text search before cosine similarity.
|
|
83
|
-
* Returns
|
|
84
|
-
*
|
|
85
|
-
*
|
|
94
|
+
* Returns up to FTS_CANDIDATE_LIMIT candidates.
|
|
95
|
+
*
|
|
96
|
+
* FTS keyword overlap can be sparse, so the fallback recency/importance scan is
|
|
97
|
+
* UNIONed with (never substituted for) the FTS hits:
|
|
98
|
+
* - >= FTS_FALLBACK_THRESHOLD FTS hits: use the FTS hits as-is (well-matched).
|
|
99
|
+
* - 0 FTS hits: use the fallback scan only.
|
|
100
|
+
* - 1..threshold-1 FTS hits: UNION the FTS hits with the fallback scan,
|
|
101
|
+
* deduplicated by id (FTS hits first), capped at FTS_CANDIDATE_LIMIT.
|
|
102
|
+
*
|
|
103
|
+
* The previous behavior REPLACED a small FTS hit set with the fallback scan,
|
|
104
|
+
* which silently dropped genuine matches that were old (>90d) and low-importance
|
|
105
|
+
* (<8) — exactly the rows the fallback's filter excludes — returning zero
|
|
106
|
+
* candidates for a query that matched only such rows.
|
|
86
107
|
*/
|
|
87
108
|
export function searchMemoryCandidatesFTS(db, projectId, queryText) {
|
|
88
109
|
const sanitized = sanitizeFtsQuery(queryText);
|
|
@@ -97,8 +118,25 @@ export function searchMemoryCandidatesFTS(db, projectId, queryText) {
|
|
|
97
118
|
// FTS5 MATCH can throw on malformed queries — fall back to full scan
|
|
98
119
|
return searchMemoryCandidates(db, projectId);
|
|
99
120
|
}
|
|
100
|
-
if (rows.length
|
|
101
|
-
return
|
|
121
|
+
if (rows.length >= FTS_FALLBACK_THRESHOLD) {
|
|
122
|
+
return dedupById(mapRows(rows));
|
|
102
123
|
}
|
|
103
|
-
|
|
124
|
+
const fallback = searchMemoryCandidates(db, projectId);
|
|
125
|
+
if (rows.length === 0) {
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
128
|
+
// UNION FTS hits (first) with the fallback scan, deduplicated by id and
|
|
129
|
+
// capped at the same total limit FTS would have returned.
|
|
130
|
+
const ftsHits = mapRows(rows);
|
|
131
|
+
const seen = new Set(ftsHits.map((candidate) => candidate.id));
|
|
132
|
+
const merged = [...ftsHits];
|
|
133
|
+
for (const candidate of fallback) {
|
|
134
|
+
if (merged.length >= FTS_CANDIDATE_LIMIT)
|
|
135
|
+
break;
|
|
136
|
+
if (seen.has(candidate.id))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(candidate.id);
|
|
139
|
+
merged.push(candidate);
|
|
140
|
+
}
|
|
141
|
+
return merged;
|
|
104
142
|
}
|
|
@@ -4,8 +4,11 @@ function getSessionEventsStatements(db) {
|
|
|
4
4
|
if (stmts)
|
|
5
5
|
return stmts;
|
|
6
6
|
stmts = {
|
|
7
|
+
// INSERT OR IGNORE so re-ingesting an event with the same logical key
|
|
8
|
+
// (project_id, session_id, event_index) — now a UNIQUE index, migration 009
|
|
9
|
+
// — is a no-op rather than a duplicate row or a PK error.
|
|
7
10
|
insertEvent: db.prepare(`
|
|
8
|
-
INSERT INTO session_events (
|
|
11
|
+
INSERT OR IGNORE INTO session_events (
|
|
9
12
|
id, project_id, session_id, event_index, event_type, payload_json, created_at
|
|
10
13
|
) VALUES (
|
|
11
14
|
@id, @project_id, @session_id, @event_index, @event_type, @payload_json,
|
|
@@ -18,12 +21,22 @@ function getSessionEventsStatements(db) {
|
|
|
18
21
|
WHERE project_id = ? AND session_id = ?
|
|
19
22
|
ORDER BY event_index ASC
|
|
20
23
|
`),
|
|
24
|
+
countAll: db.prepare("SELECT COUNT(*) AS count FROM session_events WHERE project_id = ?"),
|
|
21
25
|
};
|
|
22
26
|
sessionEventsStmtCache.set(db, stmts);
|
|
23
27
|
return stmts;
|
|
24
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Insert a session event. Returns the number of rows written (1, or 0 when the
|
|
31
|
+
* (project_id, session_id, event_index) key already exists and the insert was
|
|
32
|
+
* ignored).
|
|
33
|
+
*/
|
|
25
34
|
export function insertSessionEvent(db, input) {
|
|
26
|
-
getSessionEventsStatements(db).insertEvent.run(input);
|
|
35
|
+
return getSessionEventsStatements(db).insertEvent.run(input).changes;
|
|
36
|
+
}
|
|
37
|
+
export function countAllSessionEvents(db, projectId) {
|
|
38
|
+
const row = getSessionEventsStatements(db).countAll.get(projectId);
|
|
39
|
+
return row.count;
|
|
27
40
|
}
|
|
28
41
|
export function listSessionEventsBySession(db, projectId, sessionId) {
|
|
29
42
|
return getSessionEventsStatements(db).listBySession.all(projectId, sessionId);
|
|
@@ -19,15 +19,28 @@ export async function summarizeWithCloud(input) {
|
|
|
19
19
|
const client = new Anthropic({ apiKey: input.anthropicApiKey });
|
|
20
20
|
const response = await client.messages.create({
|
|
21
21
|
model,
|
|
22
|
-
|
|
22
|
+
// Cap the per-response token budget so a large summaryTokenCap cannot pass
|
|
23
|
+
// an out-of-range max_tokens to the API (which would error).
|
|
24
|
+
max_tokens: Math.min(Math.floor((input.summaryTokenCap ?? 4000) * 1.5), 8192),
|
|
23
25
|
system: CLOUD_SYSTEM_PROMPT,
|
|
24
26
|
messages: [{ role: "user", content: localResult.summary }],
|
|
25
27
|
});
|
|
26
28
|
// Extract text from response content blocks
|
|
27
|
-
|
|
29
|
+
let text = response.content
|
|
28
30
|
.filter((block) => block.type === "text")
|
|
29
31
|
.map((block) => block.text)
|
|
30
32
|
.join("\n");
|
|
33
|
+
// Throw on an empty response rather than storing an empty memory; the
|
|
34
|
+
// session-lifecycle retry/fallback-to-local path handles the failure.
|
|
35
|
+
if (!text || text.trim().length === 0) {
|
|
36
|
+
throw new Error("Cloud summarizer returned empty response");
|
|
37
|
+
}
|
|
38
|
+
// Cap cloud output to summaryTokenCap (rough token→char ratio) for parity
|
|
39
|
+
// with the local summarizer, so a verbose cloud response cannot blow past the
|
|
40
|
+
// configured budget.
|
|
41
|
+
if (input.summaryTokenCap && text.length > input.summaryTokenCap * 4) {
|
|
42
|
+
text = text.slice(0, input.summaryTokenCap * 4);
|
|
43
|
+
}
|
|
31
44
|
return {
|
|
32
45
|
summary: text,
|
|
33
46
|
warningCodes: localResult.warningCodes,
|
|
@@ -3,8 +3,20 @@
|
|
|
3
3
|
// a fragment of a larger secret and leave a partial body behind. All patterns are
|
|
4
4
|
// anchored with explicit literal prefixes/markers and use bounded quantifiers to
|
|
5
5
|
// avoid catastrophic backtracking (ReDoS).
|
|
6
|
-
|
|
6
|
+
//
|
|
7
|
+
// The rule set is allocated once at module load (not per call): the rules are
|
|
8
|
+
// pure, stateless closures, so hoisting avoids re-allocating 8 closures on every
|
|
9
|
+
// applyRedaction invocation — a measurable saving in batch/import/pull loops.
|
|
10
|
+
// The regex literals carry no `lastIndex` state because every pattern is used
|
|
11
|
+
// with String.prototype.replace (not stateful .test()/.exec() on a shared regex).
|
|
12
|
+
const DEFAULT_RULES = createDefaultRules();
|
|
13
|
+
function createDefaultRules() {
|
|
7
14
|
return [
|
|
15
|
+
// URL-embedded credentials: scheme://user:password@host. Run BEFORE the
|
|
16
|
+
// email rule so the `password@host` segment is collapsed here rather than
|
|
17
|
+
// partially matched as an email. Scheme + host are preserved; the
|
|
18
|
+
// user:password pair is redacted. Bounded quantifiers stay ReDoS-safe.
|
|
19
|
+
(input) => input.replace(/\b([a-z][a-z0-9+.-]{0,20}:\/\/)[^\s:/@]{1,256}:[^\s/@]{1,256}@/gi, "$1[REDACTED_CREDENTIALS]@"),
|
|
8
20
|
// Email (original rule — unchanged).
|
|
9
21
|
(input) => input.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]"),
|
|
10
22
|
// PEM private key block: ----BEGIN <type> PRIVATE KEY---- ... ----END ... ----.
|
|
@@ -14,14 +26,39 @@ function defaultRules() {
|
|
|
14
26
|
(input) => input.replace(/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\b/g, "[REDACTED_JWT]"),
|
|
15
27
|
// AWS access key id: AKIA + 16 uppercase alphanumerics.
|
|
16
28
|
(input) => input.replace(/\bAKIA[A-Z0-9]{16}\b/g, "[REDACTED_AWS_KEY]"),
|
|
17
|
-
//
|
|
18
|
-
(input) => input.replace(/\
|
|
19
|
-
//
|
|
20
|
-
(
|
|
29
|
+
// AWS temporary credentials access key id: ASIA + 16 uppercase alphanumerics.
|
|
30
|
+
(input) => input.replace(/\bASIA[A-Z0-9]{16}\b/g, "[REDACTED_AWS_KEY]"),
|
|
31
|
+
// GitHub tokens: ghp_/gho_/ghu_/ghs_/ghr_ + 36 OR MORE alphanumerics
|
|
32
|
+
// (GitHub has lengthened tokens over time; `{36,}` future-proofs the rule).
|
|
33
|
+
(input) => input.replace(/\bgh[poushr]_[A-Za-z0-9]{36,}\b/g, "[REDACTED_GITHUB_TOKEN]"),
|
|
34
|
+
// GitHub fine-grained personal access tokens: github_pat_ + long body.
|
|
35
|
+
(input) => input.replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "[REDACTED_GITHUB_TOKEN]"),
|
|
36
|
+
// Slack tokens: xoxb-/xoxp-/xoxa-/xoxr-/xoxs-/xoxe-/xoxc-/xoxt- and the
|
|
37
|
+
// app-level xapp- prefix + dash-delimited segments.
|
|
38
|
+
(input) => input.replace(/\b(?:xox[baprsect]|xapp)-[A-Za-z0-9-]{10,256}\b/g, "[REDACTED_SLACK_TOKEN]"),
|
|
39
|
+
// Stripe live keys: secret (sk_live_), restricted (rk_live_), and
|
|
40
|
+
// publishable (pk_live_) + 24 or more alphanumerics.
|
|
41
|
+
(input) => input.replace(/\b[srp]k_live_[A-Za-z0-9]{24,}\b/g, "[REDACTED_STRIPE_KEY]"),
|
|
42
|
+
// npm access tokens: npm_ + 36 alphanumerics.
|
|
43
|
+
(input) => input.replace(/\bnpm_[A-Za-z0-9]{36}\b/g, "[REDACTED_NPM_TOKEN]"),
|
|
44
|
+
// Google API keys: AIza + 35 url-safe chars (39 total).
|
|
45
|
+
(input) => input.replace(/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED_GOOGLE_API_KEY]"),
|
|
46
|
+
// OpenAI-style API key. Allow an optional internal dash segment so
|
|
47
|
+
// project-scoped keys (sk-proj-...) are fully redacted rather than leaving
|
|
48
|
+
// the project segment behind. Bounded quantifiers stay ReDoS-safe.
|
|
49
|
+
(input) => input.replace(/\bsk-(?:[a-zA-Z0-9]{1,32}-){0,4}[a-zA-Z0-9]{12,200}\b/g, "[REDACTED_API_KEY]"),
|
|
21
50
|
// Bearer token header value: "Bearer <token>" (case-insensitive), token redacted.
|
|
22
51
|
(input) => input.replace(/\bBearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, "Bearer [REDACTED_BEARER_TOKEN]"),
|
|
23
|
-
//
|
|
24
|
-
|
|
52
|
+
// Config/connection-string assignments: key=value where key is a known
|
|
53
|
+
// secret-bearing name. The key is kept and the value redacted. Covers
|
|
54
|
+
// password/secret plus api_key/apikey/token/access_token/pwd in URL query
|
|
55
|
+
// strings and config files. The `=` separator may carry surrounding spaces.
|
|
56
|
+
(input) => input.replace(/\b(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|token)\s*=\s*([^\s"'&;]+)/gi, "$1=[REDACTED]"),
|
|
57
|
+
// JSON-form secret assignments: "key": "value" for known secret-bearing
|
|
58
|
+
// keys. Complements the key=value rule above so secrets embedded in JSON
|
|
59
|
+
// payloads (config files, logged request bodies) are redacted too. The key
|
|
60
|
+
// is preserved; the quoted value is collapsed.
|
|
61
|
+
(input) => input.replace(/"(password|secret|api_key|token|access_token|auth_token|client_secret|private_key|pwd|passwd)"\s*:\s*"[^"]{4,}"/gi, '"$1": "[REDACTED]"'),
|
|
25
62
|
];
|
|
26
63
|
}
|
|
27
64
|
export function applyRedaction(input, options) {
|
|
@@ -33,7 +70,7 @@ export function applyRedaction(input, options) {
|
|
|
33
70
|
}
|
|
34
71
|
let text = input;
|
|
35
72
|
const warningCodes = [];
|
|
36
|
-
for (const rule of options.rules ??
|
|
73
|
+
for (const rule of options.rules ?? DEFAULT_RULES) {
|
|
37
74
|
try {
|
|
38
75
|
text = rule(text);
|
|
39
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sessionmem",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Local-first MCP memory layer for coding agents across Claude Code, Codex, Cursor, Cline, Windsurf, and other MCP-compatible hosts.",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lint": "eslint .",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"benchmark": "node scripts/benchmark.mjs",
|
|
31
|
-
"postversion": "node -e \"const v=require('./package.json').version; ['package/package.json','.claude-plugin/plugin.json','server.json'].forEach(f=>{const o=require('./'+f);o.version=v;if(o.packages&&o.packages[0])o.packages[0].version=v;require('fs').writeFileSync('./'+f,JSON.stringify(o,null,2)+'\\n')})\""
|
|
31
|
+
"postversion": "node -e \"const v=require('./package.json').version; ['package/package.json','.claude-plugin/plugin.json','server.json'].forEach(f=>{const o=require('./'+f);o.version=v;if(o.packages&&o.packages[0])o.packages[0].version=v;require('fs').writeFileSync('./'+f,JSON.stringify(o,null,2)+'\\n')})\" && npm run build"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@anthropic-ai/sdk": "^0.105.0",
|