sessionmem 1.0.5 → 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.
Files changed (58) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +372 -365
  3. package/dist/adapters/capabilities/fallbackTools.js +33 -18
  4. package/dist/adapters/claudeMdInjector.js +164 -0
  5. package/dist/adapters/factory.js +68 -9
  6. package/dist/adapters/generic.js +221 -15
  7. package/dist/adapters/global/antigravity.js +14 -7
  8. package/dist/adapters/global/claudeCode.js +46 -10
  9. package/dist/adapters/global/codex.js +73 -13
  10. package/dist/adapters/global/qcoder.js +18 -5
  11. package/dist/adapters/ide/cline.js +54 -9
  12. package/dist/adapters/ide/cursor.js +15 -13
  13. package/dist/adapters/ide/installer.js +201 -8
  14. package/dist/adapters/ide/windsurf.js +14 -13
  15. package/dist/adapters/tools/ping.js +4 -1
  16. package/dist/cli/commands/config.js +10 -1
  17. package/dist/cli/commands/import.js +6 -1
  18. package/dist/cli/commands/install.js +63 -5
  19. package/dist/cli/commands/ping.js +42 -8
  20. package/dist/cli/commands/reEmbed.js +48 -0
  21. package/dist/cli/commands/run.js +18 -2
  22. package/dist/cli/commands/savings.js +91 -0
  23. package/dist/cli/commands/sessionEnd.js +124 -0
  24. package/dist/cli/commands/sessionStart.js +52 -0
  25. package/dist/cli/commands/sync.js +39 -9
  26. package/dist/cli/commands/uninstall.js +37 -1
  27. package/dist/cli/context.js +14 -18
  28. package/dist/cli/index.js +30 -4
  29. package/dist/cli/output.js +11 -3
  30. package/dist/cli/projectId.js +69 -0
  31. package/dist/core/api/contracts.js +182 -45
  32. package/dist/core/api/errors.js +4 -7
  33. package/dist/core/api/memoryCoreService.js +409 -240
  34. package/dist/core/api/sessionLifecycleService.js +20 -2
  35. package/dist/core/config/policyConfig.js +53 -6
  36. package/dist/core/injection/formatStartupInjection.js +55 -10
  37. package/dist/core/injection/tokenBudget.js +8 -0
  38. package/dist/core/retrieve/importance.js +4 -3
  39. package/dist/core/retrieve/recencyBands.js +6 -10
  40. package/dist/core/retrieve/retrieveMemories.js +19 -4
  41. package/dist/core/retrieve/score.js +11 -1
  42. package/dist/core/schema/migrations/005_team_provenance.sql +14 -9
  43. package/dist/core/schema/migrations/006_access_pattern_boosting.sql +10 -0
  44. package/dist/core/schema/migrations/007_feedback_manual_delete.sql +23 -0
  45. package/dist/core/schema/migrations/008_fts5_search.sql +37 -0
  46. package/dist/core/schema/migrations/009_session_events_unique.sql +24 -0
  47. package/dist/core/schema/runMigrations.js +64 -2
  48. package/dist/core/storage/db.js +6 -0
  49. package/dist/core/storage/memoryFeedbackRepo.js +14 -4
  50. package/dist/core/storage/memoryRepo.js +292 -121
  51. package/dist/core/storage/memorySearchRepo.js +125 -13
  52. package/dist/core/storage/sessionEventsRepo.js +33 -10
  53. package/dist/core/storage/summarizationFailuresRepo.js +36 -26
  54. package/dist/core/storage/tokenSavingsRepo.js +20 -0
  55. package/dist/core/summarize/cloudSummarizer.js +34 -5
  56. package/dist/core/summarize/localSummarizer.js +1 -10
  57. package/dist/core/summarize/redaction.js +45 -8
  58. package/package.json +50 -48
@@ -1,12 +1,10 @@
1
- function toParams(input) {
2
- return {
3
- ...input,
4
- created_at: input.created_at ?? null,
5
- updated_at: input.updated_at ?? null,
6
- };
7
- }
8
- export function insertSummarizationFailure(db, input) {
9
- const stmt = db.prepare(`
1
+ const sumFailStmtCache = new WeakMap();
2
+ function getSumFailStatements(db) {
3
+ let stmts = sumFailStmtCache.get(db);
4
+ if (stmts)
5
+ return stmts;
6
+ stmts = {
7
+ insertFailure: db.prepare(`
10
8
  INSERT INTO summarization_failures (
11
9
  id, project_id, session_id, source_adapter, reason, attempt_count, last_error_json, created_at, updated_at
12
10
  ) VALUES (
@@ -14,26 +12,38 @@ export function insertSummarizationFailure(db, input) {
14
12
  COALESCE(@created_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
15
13
  COALESCE(@updated_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
16
14
  )
17
- `);
18
- stmt.run(toParams(input));
19
- }
20
- export function listSummarizationFailures(db, projectId, sessionId) {
21
- if (sessionId) {
22
- const stmt = db.prepare(`
23
- SELECT
24
- id, project_id, session_id, source_adapter, reason, attempt_count, last_error_json, created_at, updated_at
25
- FROM summarization_failures
26
- WHERE project_id = ? AND session_id = ?
27
- ORDER BY updated_at DESC
28
- `);
29
- return stmt.all(projectId, sessionId);
30
- }
31
- const stmt = db.prepare(`
15
+ `),
16
+ listByProjectAndSession: db.prepare(`
17
+ SELECT
18
+ id, project_id, session_id, source_adapter, reason, attempt_count, last_error_json, created_at, updated_at
19
+ FROM summarization_failures
20
+ WHERE project_id = ? AND session_id = ?
21
+ ORDER BY updated_at DESC
22
+ `),
23
+ listByProject: db.prepare(`
32
24
  SELECT
33
25
  id, project_id, session_id, source_adapter, reason, attempt_count, last_error_json, created_at, updated_at
34
26
  FROM summarization_failures
35
27
  WHERE project_id = ?
36
28
  ORDER BY updated_at DESC
37
- `);
38
- return stmt.all(projectId);
29
+ `),
30
+ };
31
+ sumFailStmtCache.set(db, stmts);
32
+ return stmts;
33
+ }
34
+ function toParams(input) {
35
+ return {
36
+ ...input,
37
+ created_at: input.created_at ?? null,
38
+ updated_at: input.updated_at ?? null,
39
+ };
40
+ }
41
+ export function insertSummarizationFailure(db, input) {
42
+ getSumFailStatements(db).insertFailure.run(toParams(input));
43
+ }
44
+ export function listSummarizationFailures(db, projectId, sessionId) {
45
+ if (sessionId) {
46
+ return getSumFailStatements(db).listByProjectAndSession.all(projectId, sessionId);
47
+ }
48
+ return getSumFailStatements(db).listByProject.all(projectId);
39
49
  }
@@ -0,0 +1,20 @@
1
+ const tokenSavingsStmtCache = new WeakMap();
2
+ function getTokenSavingsStatements(db) {
3
+ let stmts = tokenSavingsStmtCache.get(db);
4
+ if (stmts)
5
+ return stmts;
6
+ stmts = {
7
+ countDistinctSessions: db.prepare("SELECT COUNT(DISTINCT session_id) AS count FROM session_events WHERE project_id = ?"),
8
+ listPayloads: db.prepare("SELECT payload_json FROM session_events WHERE project_id = ?"),
9
+ };
10
+ tokenSavingsStmtCache.set(db, stmts);
11
+ return stmts;
12
+ }
13
+ export function countDistinctSessions(db, projectId) {
14
+ const row = getTokenSavingsStatements(db).countDistinctSessions.get(projectId);
15
+ return row.count;
16
+ }
17
+ export function listEventPayloads(db, projectId) {
18
+ const rows = getTokenSavingsStatements(db).listPayloads.all(projectId);
19
+ return rows.map(r => r.payload_json);
20
+ }
@@ -1,19 +1,48 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
1
2
  import { summarizeLocalSessionEvents } from "./localSummarizer.js";
2
- const DEFAULT_CLOUD_MODEL = "claude-sonnet-4-20250514";
3
+ import { DEFAULT_SUMMARIZER_MODEL } from "../config/policyConfig.js";
4
+ const CLOUD_SYSTEM_PROMPT = "You are a memory compressor for AI coding sessions. Given this session transcript summary, produce a compact, high-signal list of facts, decisions, and context that should be remembered. Be extremely concise.";
3
5
  export async function summarizeWithCloud(input) {
4
6
  if (!input.anthropicApiKey.trim()) {
5
7
  throw new Error("Missing anthropicApiKey");
6
8
  }
7
- const result = await summarizeLocalSessionEvents({
9
+ // Step 1: Preprocess via local summarizer (extract, redact, structure)
10
+ const localResult = await summarizeLocalSessionEvents({
8
11
  events: input.events,
9
12
  summaryTokenCap: input.summaryTokenCap,
10
13
  redactionEnabled: input.redactionEnabled,
11
14
  factMode: input.factMode,
12
15
  redactionRules: input.redactionRules,
13
16
  });
14
- const modelTag = input.model ?? DEFAULT_CLOUD_MODEL;
17
+ // Step 2: Send preprocessed summary to Claude for compression
18
+ const model = input.model ?? DEFAULT_SUMMARIZER_MODEL;
19
+ const client = new Anthropic({ apiKey: input.anthropicApiKey });
20
+ const response = await client.messages.create({
21
+ model,
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),
25
+ system: CLOUD_SYSTEM_PROMPT,
26
+ messages: [{ role: "user", content: localResult.summary }],
27
+ });
28
+ // Extract text from response content blocks
29
+ let text = response.content
30
+ .filter((block) => block.type === "text")
31
+ .map((block) => block.text)
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
+ }
15
44
  return {
16
- summary: `[model:${modelTag}] ${result.summary}`,
17
- warningCodes: result.warningCodes,
45
+ summary: text,
46
+ warningCodes: localResult.warningCodes,
18
47
  };
19
48
  }
@@ -1,16 +1,7 @@
1
1
  import { normalizeEmbeddingText } from "../embed/textNormalize.js";
2
+ import { capTokens, countTokens } from "../injection/tokenBudget.js";
2
3
  import { applyRedaction } from "./redaction.js";
3
4
  import { buildStructuredSummary } from "./summaryShape.js";
4
- function countTokens(text) {
5
- return text.trim().split(/\s+/).filter(Boolean).length;
6
- }
7
- function capTokens(text, cap) {
8
- const tokens = text.trim().split(/\s+/).filter(Boolean);
9
- if (tokens.length <= cap) {
10
- return text;
11
- }
12
- return `${tokens.slice(0, cap).join(" ")} ...`;
13
- }
14
5
  export async function summarizeLocalSessionEvents(input) {
15
6
  const structured = buildStructuredSummary(input.events, {
16
7
  factMode: input.factMode,
@@ -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
- function defaultRules() {
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
- // GitHub tokens: ghp_/gho_/ghu_/ghs_/ghr_ + 36 alphanumerics.
18
- (input) => input.replace(/\bgh[poushr]_[A-Za-z0-9]{36}\b/g, "[REDACTED_GITHUB_TOKEN]"),
19
- // OpenAI-style API key (original rule unchanged).
20
- (input) => input.replace(/sk-[a-zA-Z0-9]{12,}/g, "[REDACTED_API_KEY]"),
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
- // Connection-string assignment: password=/secret= value -> key kept, value redacted.
24
- (input) => input.replace(/\b(password|secret)=([^\s"'&;]+)/gi, "$1=[REDACTED]"),
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 ?? defaultRules()) {
73
+ for (const rule of options.rules ?? DEFAULT_RULES) {
37
74
  try {
38
75
  text = rule(text);
39
76
  }
package/package.json CHANGED
@@ -1,48 +1,50 @@
1
- {
2
- "name": "sessionmem",
3
- "version": "1.0.5",
4
- "private": false,
5
- "type": "module",
6
- "description": "Local-first MCP memory layer for coding agents across Claude Code, Codex, Cursor, Cline, Windsurf, and other MCP-compatible hosts.",
7
- "license": "MIT",
8
- "author": "kavishdua",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/catfish-1234/sessionmem.git"
12
- },
13
- "mcpName": "io.github.catfish-1234/sessionmem",
14
- "files": [
15
- "dist"
16
- ],
17
- "publishConfig": {
18
- "access": "public"
19
- },
20
- "bin": {
21
- "sessionmem": "./dist/cli/index.js"
22
- },
23
- "scripts": {
24
- "build": "tsc && node scripts/copy-migrations.mjs",
25
- "prepack": "npm run build",
26
- "test": "vitest run --reporter=dot",
27
- "test:schema": "vitest run tests/integration/storage/schema.spec.ts --reporter=dot",
28
- "lint": "eslint .",
29
- "typecheck": "tsc --noEmit",
30
- "benchmark": "node scripts/benchmark.mjs"
31
- },
32
- "dependencies": {
33
- "@modelcontextprotocol/sdk": "^1.29.0",
34
- "better-sqlite3": "^12.4.1",
35
- "commander": "^15.0.0",
36
- "js-tiktoken": "^1.0.21",
37
- "zod": "^4.4.3"
38
- },
39
- "devDependencies": {
40
- "@eslint/js": "^10.0.1",
41
- "@types/better-sqlite3": "^7.6.13",
42
- "eslint": "^10.4.1",
43
- "globals": "^17.6.0",
44
- "typescript": "^6.0.3",
45
- "typescript-eslint": "^8.61.0",
46
- "vitest": "^4.1.8"
47
- }
48
- }
1
+ {
2
+ "name": "sessionmem",
3
+ "version": "1.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Local-first MCP memory layer for coding agents across Claude Code, Codex, Cursor, Cline, Windsurf, and other MCP-compatible hosts.",
7
+ "license": "MIT",
8
+ "author": "kavishdua",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/catfish-1234/sessionmem.git"
12
+ },
13
+ "mcpName": "io.github.catfish-1234/sessionmem",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "bin": {
21
+ "sessionmem": "./dist/cli/index.js"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc && node scripts/copy-migrations.mjs",
25
+ "prepack": "npm run build",
26
+ "test": "vitest run --reporter=dot",
27
+ "test:schema": "vitest run tests/integration/storage/schema.spec.ts --reporter=dot",
28
+ "lint": "eslint .",
29
+ "typecheck": "tsc --noEmit",
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')})\" && npm run build"
32
+ },
33
+ "dependencies": {
34
+ "@anthropic-ai/sdk": "^0.105.0",
35
+ "@modelcontextprotocol/sdk": "^1.29.0",
36
+ "better-sqlite3": "^12.4.1",
37
+ "commander": "^15.0.0",
38
+ "js-tiktoken": "^1.0.21",
39
+ "zod": "^4.4.3"
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/js": "^10.0.1",
43
+ "@types/better-sqlite3": "^7.6.13",
44
+ "eslint": "^10.4.1",
45
+ "globals": "^17.6.0",
46
+ "typescript": "^6.0.3",
47
+ "typescript-eslint": "^8.61.0",
48
+ "vitest": "^4.1.8"
49
+ }
50
+ }