@bcmyguest/openmem 0.0.0-stage → 0.8.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/.agents/skills/openmem/SKILL.md +34 -0
- package/LICENSE +190 -0
- package/README.md +140 -2
- package/bin/hindsight-project-index +530 -0
- package/cli/coding-agent/access-policy.mjs +38 -0
- package/cli/coding-agent/agents.mjs +34 -0
- package/cli/coding-agent/bank-from-cwd.mjs +140 -0
- package/cli/coding-agent/capabilities.mjs +34 -0
- package/cli/coding-agent/context-store.mjs +81 -0
- package/cli/coding-agent/context-trim.mjs +897 -0
- package/cli/coding-agent/file-io.mjs +72 -0
- package/cli/coding-agent/hook-harnesses.mjs +88 -0
- package/cli/coding-agent/hook-runner.mjs +284 -0
- package/cli/coding-agent/install-access.mjs +119 -0
- package/cli/coding-agent/install-context-proxy.mjs +333 -0
- package/cli/coding-agent/install-rtk.mjs +60 -0
- package/cli/coding-agent/install-shim.mjs +201 -0
- package/cli/coding-agent/opencode-config.mjs +107 -0
- package/cli/coding-agent/opencode-plugin.mjs +132 -0
- package/cli/coding-agent/opencode2-plugin/index.js +299 -0
- package/cli/coding-agent/recall-frame.mjs +29 -0
- package/cli/coding-agent/remove-upstream-access.mjs +122 -0
- package/cli/coding-agent/runtime.mjs +40 -0
- package/cli/coding-agent/session-db.mjs +626 -0
- package/cli/coding-agent/session-facade.mjs +86 -0
- package/cli/coding-agent/toml-edit.mjs +161 -0
- package/cli/cognee-mcp.mjs +260 -0
- package/cli/context-management.mjs +254 -0
- package/cli/feed-sync.mjs +27 -0
- package/cli/install-coding-agent.mjs +298 -0
- package/cli/install-memory-mcp.mjs +426 -0
- package/cli/is-main.mjs +18 -0
- package/cli/mcp/context-sql-runner.mjs +58 -0
- package/cli/mcp/context-store.mjs +670 -0
- package/cli/mcp/context.mjs +61 -0
- package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
- package/cli/mcp/export/cognee.mjs +119 -0
- package/cli/mcp/export/hindsight.mjs +48 -0
- package/cli/mcp/export/index.mjs +26 -0
- package/cli/mcp/export/mem0.mjs +31 -0
- package/cli/mcp/knowledge.mjs +333 -0
- package/cli/mcp/llm-synthesize.mjs +50 -0
- package/cli/mcp/mem0-scope-id.mjs +22 -0
- package/cli/mcp/memory-facade.mjs +521 -0
- package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
- package/cli/mcp/notebooklm-banks.mjs +21 -0
- package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
- package/cli/mcp/notebooklm-provider.mjs +225 -0
- package/cli/mcp/pmm-records.mjs +23 -0
- package/cli/mcp/portability.mjs +158 -0
- package/cli/mcp/stdio.mjs +65 -0
- package/cli/mcp/unified-mcp-install.mjs +65 -0
- package/cli/mem0-mcp.mjs +108 -0
- package/cli/notebooklm-mcp.mjs +236 -0
- package/cli/openmem-mcp.mjs +38 -0
- package/cli/openmem.mjs +222 -0
- package/cli/roots.mjs +85 -0
- package/cli/schedule.mjs +176 -0
- package/cli/stack.mjs +438 -0
- package/cli/switch-memory-provider.mjs +358 -0
- package/cli/wiring-report.mjs +125 -0
- package/compose.bench.yaml +12 -0
- package/compose.yaml +227 -0
- package/config/coding-agent.json +13 -0
- package/config/hindsight.env.example +26 -0
- package/config/index.yml +54 -0
- package/config/litellm.lemond.yaml +40 -0
- package/config/litellm.ollama.yaml +33 -0
- package/config/notebooklm.yml +33 -0
- package/config/openmem.toml.example +124 -0
- package/docs/OPENMEM.md +23 -0
- package/docs/architecture.md +245 -0
- package/docs/assets/README.md +10 -0
- package/docs/assets/setup-demo.gif +0 -0
- package/docs/coding-agent-hooks.md +75 -0
- package/docs/coding-agent-scopes.md +22 -0
- package/docs/coding-agents.md +71 -0
- package/docs/cognee.md +37 -0
- package/docs/compatibility.md +80 -0
- package/docs/concepts.md +135 -0
- package/docs/context-management.md +255 -0
- package/docs/context-proxy.md +115 -0
- package/docs/context-research.md +215 -0
- package/docs/context-store.md +277 -0
- package/docs/hindsight.md +77 -0
- package/docs/index-sources-roadmap.md +38 -0
- package/docs/index-sources.md +39 -0
- package/docs/indexer-sinks.md +122 -0
- package/docs/indexing.md +57 -0
- package/docs/manual-stack-setup.md +74 -0
- package/docs/mcp-install-guide.md +4 -0
- package/docs/mcp-setup.md +104 -0
- package/docs/mem0.md +141 -0
- package/docs/memory-portability.md +73 -0
- package/docs/memory-providers.md +246 -0
- package/docs/meridian-litellm.md +198 -0
- package/docs/model-configuration.md +106 -0
- package/docs/notebooklm.md +83 -0
- package/docs/npx-install.md +172 -0
- package/docs/openmem-config.md +194 -0
- package/docs/provider-wizard-hooks.md +222 -0
- package/docs/quickstart.md +107 -0
- package/docs/setup-wizard.md +117 -0
- package/docs/sources-feeds.md +39 -0
- package/docs/sources-readwise.md +23 -0
- package/docs/sources-zotero.md +22 -0
- package/glama.json +4 -0
- package/indexer/Dockerfile +38 -0
- package/indexer/code_outline.py +146 -0
- package/indexer/feed_sync.py +107 -0
- package/indexer/index_config.py +290 -0
- package/indexer/pytest.ini +5 -0
- package/indexer/sinks/__init__.py +148 -0
- package/indexer/sinks/_http.py +44 -0
- package/indexer/sinks/cognee.py +329 -0
- package/indexer/sinks/hindsight.py +107 -0
- package/indexer/sinks/mem0.py +247 -0
- package/indexer/watch.py +240 -0
- package/package.json +96 -4
- package/providers/_template/compose.yaml +52 -0
- package/providers/cognee/OPERATOR.md +82 -0
- package/providers/cognee/compose.yaml +89 -0
- package/providers/cognee/lib/clack.mjs +157 -0
- package/providers/cognee/lib/coding-agent.mjs +26 -0
- package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
- package/providers/cognee/lib/config.mjs +98 -0
- package/providers/cognee/lib/env-file.mjs +194 -0
- package/providers/cognee/lib/env-setup.mjs +83 -0
- package/providers/cognee/lib/llm-env.mjs +382 -0
- package/providers/cognee/lib/ports.mjs +25 -0
- package/providers/cognee/lib/smoke.mjs +246 -0
- package/providers/cognee/lib/stack-env.mjs +134 -0
- package/providers/cognee/lib/state.mjs +101 -0
- package/providers/cognee/lib/summary.mjs +56 -0
- package/providers/cognee/lib/validate.mjs +97 -0
- package/providers/cognee/manifest.json +56 -0
- package/providers/cognee/wizard.mjs +134 -0
- package/providers/graphiti/manifest.json +15 -0
- package/providers/hindsight/lib/clack.mjs +347 -0
- package/providers/hindsight/lib/coding-agent.mjs +204 -0
- package/providers/hindsight/lib/config.mjs +79 -0
- package/providers/hindsight/lib/env-setup.mjs +72 -0
- package/providers/hindsight/lib/litellm-routes.mjs +196 -0
- package/providers/hindsight/lib/meridian.mjs +307 -0
- package/providers/hindsight/lib/model-backend.mjs +243 -0
- package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
- package/providers/hindsight/lib/ollama-compose.mjs +51 -0
- package/providers/hindsight/lib/ports.mjs +30 -0
- package/providers/hindsight/lib/probe.mjs +54 -0
- package/providers/hindsight/lib/stack-env.mjs +125 -0
- package/providers/hindsight/lib/state.mjs +80 -0
- package/providers/hindsight/lib/summary.mjs +57 -0
- package/providers/hindsight/lib/validate.mjs +81 -0
- package/providers/hindsight/manifest.json +58 -0
- package/providers/hindsight/wizard.mjs +150 -0
- package/providers/letta/manifest.json +29 -0
- package/providers/mem0/OPERATOR.md +143 -0
- package/providers/mem0/compose.yaml +79 -0
- package/providers/mem0/lib/coding-agent.mjs +58 -0
- package/providers/mem0/lib/config.mjs +45 -0
- package/providers/mem0/lib/configure.mjs +201 -0
- package/providers/mem0/lib/dotenv.mjs +71 -0
- package/providers/mem0/lib/env-file.mjs +110 -0
- package/providers/mem0/lib/http.mjs +69 -0
- package/providers/mem0/lib/litellm.mjs +457 -0
- package/providers/mem0/lib/mcp-install.mjs +165 -0
- package/providers/mem0/lib/secret.mjs +30 -0
- package/providers/mem0/lib/smoke.mjs +179 -0
- package/providers/mem0/lib/stack-env.mjs +255 -0
- package/providers/mem0/lib/state.mjs +67 -0
- package/providers/mem0/lib/summary.mjs +88 -0
- package/providers/mem0/lib/url.mjs +106 -0
- package/providers/mem0/lib/validate.mjs +158 -0
- package/providers/mem0/manifest.json +47 -0
- package/providers/mem0/wizard.mjs +146 -0
- package/providers/zep/manifest.json +29 -0
- package/proxy/config.yaml +33 -0
- package/proxy/openmem_context/__init__.py +6 -0
- package/proxy/openmem_context/adapters.py +504 -0
- package/proxy/openmem_context/compress.py +53 -0
- package/proxy/openmem_context/core.py +606 -0
- package/proxy/openmem_context/embed.py +212 -0
- package/proxy/openmem_context/recall.py +49 -0
- package/proxy/openmem_context/session.py +70 -0
- package/proxy/openmem_context/sessiondb.py +276 -0
- package/proxy/openmem_context/settings.py +134 -0
- package/proxy/openmem_context/store.py +129 -0
- package/proxy/openmem_context/tokens.py +123 -0
- package/proxy/openmem_context/ttl.py +78 -0
- package/proxy/openmem_context_hook.py +34 -0
- package/scripts/check-pack.mjs +219 -0
- package/scripts/gen-masks.mjs +83 -0
- package/scripts/gen-masks.sh +4 -0
- package/scripts/promotion-provider-smoke.mjs +34 -0
- package/scripts/run-setup.sh +30 -0
- package/scripts/setup.mjs +107 -0
- package/scripts/setup.sh +5 -0
- package/scripts/spin-up.sh +45 -0
- package/scripts/wizard.sh +5 -0
- package/server.json +34 -0
- package/sync/Dockerfile +27 -0
- package/sync/memory_backend.py +87 -0
- package/sync/notebooklm_sync.py +664 -0
- package/wizard/bin/pm.mjs +97 -0
- package/wizard/bin/setup.mjs +33 -0
- package/wizard/lib/compose-version.mjs +32 -0
- package/wizard/lib/compose.mjs +70 -0
- package/wizard/lib/context-step.mjs +140 -0
- package/wizard/lib/curl.mjs +103 -0
- package/wizard/lib/docker-build.mjs +148 -0
- package/wizard/lib/exec.mjs +41 -0
- package/wizard/lib/indexer.mjs +6 -0
- package/wizard/lib/litellm-env.mjs +55 -0
- package/wizard/lib/load-machine-env.mjs +4 -0
- package/wizard/lib/log.mjs +32 -0
- package/wizard/lib/notebooklm-agent-access.mjs +92 -0
- package/wizard/lib/notebooklm.mjs +214 -0
- package/wizard/lib/parse-args.mjs +87 -0
- package/wizard/lib/paths.mjs +39 -0
- package/wizard/lib/pm-config.mjs +371 -0
- package/wizard/lib/ports.mjs +94 -0
- package/wizard/lib/prompts.mjs +85 -0
- package/wizard/lib/provider-wizard/helpers.mjs +98 -0
- package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
- package/wizard/lib/providers/registry.mjs +262 -0
- package/wizard/lib/run-clack.mjs +108 -0
- package/wizard/lib/run-wizard.mjs +750 -0
- package/wizard/lib/validate-setup.mjs +62 -0
- package/wizard/lib/wizard-action.mjs +34 -0
- package/wizard/lib/wizard-conf.mjs +71 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseDocument } from "yaml";
|
|
6
|
+
|
|
7
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
|
|
9
|
+
function validGitEntry(root) {
|
|
10
|
+
const entry = path.join(root, ".git");
|
|
11
|
+
try {
|
|
12
|
+
const stat = fs.statSync(entry);
|
|
13
|
+
if (stat.isDirectory()) return fs.existsSync(path.join(entry, "HEAD")) && fs.existsSync(path.join(entry, "config"));
|
|
14
|
+
if (!stat.isFile()) return false;
|
|
15
|
+
const match = /^gitdir:\s*(.+)\s*$/m.exec(fs.readFileSync(entry, "utf8"));
|
|
16
|
+
if (!match) return false;
|
|
17
|
+
const gitDir = path.resolve(root, match[1]);
|
|
18
|
+
return fs.statSync(gitDir).isDirectory()
|
|
19
|
+
&& (fs.existsSync(path.join(gitDir, "HEAD")) || fs.existsSync(path.join(gitDir, "commondir")));
|
|
20
|
+
} catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function projectRoot(cwd) {
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.statSync(cwd).isDirectory()) return cwd;
|
|
26
|
+
} catch { return cwd; }
|
|
27
|
+
let candidate = cwd;
|
|
28
|
+
while (true) {
|
|
29
|
+
if (validGitEntry(candidate)) return candidate;
|
|
30
|
+
const parent = path.dirname(candidate);
|
|
31
|
+
if (parent === candidate) return cwd;
|
|
32
|
+
candidate = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function projectName(root) {
|
|
37
|
+
try {
|
|
38
|
+
const gitEntry = path.join(root, ".git");
|
|
39
|
+
const stat = fs.statSync(gitEntry);
|
|
40
|
+
let gitDir = gitEntry;
|
|
41
|
+
if (stat.isFile()) {
|
|
42
|
+
const match = /^gitdir:\s*(.+)\s*$/m.exec(fs.readFileSync(gitEntry, "utf8"));
|
|
43
|
+
if (match) gitDir = path.resolve(root, match[1]);
|
|
44
|
+
}
|
|
45
|
+
const commonFile = path.join(gitDir, "commondir");
|
|
46
|
+
const commonPath = fs.existsSync(commonFile)
|
|
47
|
+
? path.resolve(gitDir, fs.readFileSync(commonFile, "utf8").trim())
|
|
48
|
+
: gitDir;
|
|
49
|
+
// Only linked worktrees use a shared project's name. Submodules retain their own repository name.
|
|
50
|
+
if (path.basename(path.dirname(gitDir)) === "worktrees" && path.basename(commonPath) === ".git") {
|
|
51
|
+
return path.basename(path.dirname(commonPath));
|
|
52
|
+
}
|
|
53
|
+
} catch { /* non-git workspace */ }
|
|
54
|
+
return path.basename(root);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function expandPrefix(prefix, home) {
|
|
58
|
+
if (typeof prefix !== "string" || !prefix.trim()) return null;
|
|
59
|
+
let expanded = prefix;
|
|
60
|
+
if (prefix === "~" || prefix.startsWith("~/")) {
|
|
61
|
+
expanded = prefix === "~" ? home : path.join(home, prefix.slice(2));
|
|
62
|
+
} else if (prefix.startsWith("~")) {
|
|
63
|
+
const slash = prefix.indexOf(path.sep);
|
|
64
|
+
const username = slash < 0 ? prefix.slice(1) : prefix.slice(1, slash);
|
|
65
|
+
if (!username) return null;
|
|
66
|
+
let userHome = null;
|
|
67
|
+
try {
|
|
68
|
+
userHome = fs.readFileSync("/etc/passwd", "utf8").split(/\r?\n/)
|
|
69
|
+
.map((line) => line.split(":"))
|
|
70
|
+
.find((fields) => fields[0] === username)?.[5] || null;
|
|
71
|
+
} catch { /* passwd is not available on every platform */ }
|
|
72
|
+
if (!userHome && username === os.userInfo().username) userHome = home;
|
|
73
|
+
if (!userHome) return null;
|
|
74
|
+
expanded = slash < 0 ? userHome : path.join(userHome, prefix.slice(slash + 1));
|
|
75
|
+
}
|
|
76
|
+
if (!path.isAbsolute(expanded)) return null;
|
|
77
|
+
return path.resolve(expanded);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function matchingBank(target, mappings, home) {
|
|
81
|
+
if (!mappings || typeof mappings !== "object" || Array.isArray(mappings)) return null;
|
|
82
|
+
return Object.entries(mappings)
|
|
83
|
+
.filter(([prefix, bank]) => typeof bank === "string" && bank.trim() && expandPrefix(prefix, home))
|
|
84
|
+
.map(([prefix, bank]) => [expandPrefix(prefix, home), bank.trim()])
|
|
85
|
+
.filter(([prefix]) => target === prefix || target.startsWith(`${prefix}${path.sep}`))
|
|
86
|
+
.sort(([a], [b]) => b.length - a.length)[0]?.[1] || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readJson(file) {
|
|
90
|
+
try {
|
|
91
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
92
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
93
|
+
} catch { return {}; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function yamlBanks(file, home) {
|
|
97
|
+
const source = fs.readFileSync(file, "utf8");
|
|
98
|
+
const document = parseDocument(source, { uniqueKeys: true, version: "1.1" });
|
|
99
|
+
if (document.errors.length) throw new Error(`${file}: ${document.errors[0].message}`);
|
|
100
|
+
const data = document.toJS() ?? {};
|
|
101
|
+
if (typeof data !== "object" || Array.isArray(data)) throw new Error(`${file}: expected a YAML mapping`);
|
|
102
|
+
const banks = Object.hasOwn(data, "banks")
|
|
103
|
+
? data.banks
|
|
104
|
+
: { "~/Documents/sample-project": "coding-agent::documents-sample-project" };
|
|
105
|
+
if (!banks || typeof banks !== "object" || Array.isArray(banks)) throw new Error(`${file}: banks must be a mapping`);
|
|
106
|
+
const result = {};
|
|
107
|
+
for (const [key, value] of Object.entries(banks)) {
|
|
108
|
+
if (typeof key !== "string" || !key.trim()) throw new Error(`${file}: banks key must be a non-empty string`);
|
|
109
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${file}: banks[${key}] must be a non-empty bank id string`);
|
|
110
|
+
const prefix = expandPrefix(key, home);
|
|
111
|
+
if (!prefix) throw new Error(`${file}: banks key must be an absolute path: ${key}`);
|
|
112
|
+
result[prefix] = value;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function configuredBanks(home, env) {
|
|
118
|
+
const override = env.HINDSIGHT_INDEX_CONFIG;
|
|
119
|
+
const user = path.join(home, ".config", "hindsight", "index.yml");
|
|
120
|
+
const configFile = override ? (expandPrefix(override, home) || path.resolve(override))
|
|
121
|
+
: fs.existsSync(user) ? user : path.join(PKG_ROOT, "config", "index.yml");
|
|
122
|
+
return yamlBanks(configFile, home);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Map a workspace cwd to the indexer's project bank. */
|
|
126
|
+
export function bankFromCwd(cwd, options = {}) {
|
|
127
|
+
const dir = path.resolve(String(cwd || process.cwd()));
|
|
128
|
+
const root = projectRoot(dir);
|
|
129
|
+
const home = options.home || process.env.HOME || os.homedir();
|
|
130
|
+
const env = options.env || process.env;
|
|
131
|
+
const indexBanks = options.banks || configuredBanks(home, env);
|
|
132
|
+
const personalConfig = readJson(path.join(home, ".config", "personal-memory", "coding-agent.json"));
|
|
133
|
+
const legacyConfig = readJson(path.join(home, ".hindsight", "coding-agent.json"));
|
|
134
|
+
const mappingTarget = root === dir ? dir : root;
|
|
135
|
+
for (const mapping of [indexBanks, options.mapPathToBank || personalConfig.mapPathToBank, legacyConfig.mapPathToBank]) {
|
|
136
|
+
const bank = matchingBank(mappingTarget, mapping, home);
|
|
137
|
+
if (bank) return bank;
|
|
138
|
+
}
|
|
139
|
+
return `coding-agent::${projectName(root) || "workspace"}`;
|
|
140
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Session-memory hook parity vs Hindsight plugin (non-Hindsight uses MCP shim). */
|
|
2
|
+
|
|
3
|
+
import { CODING_AGENTS, DEFAULT_AGENT_IDS } from "./agents.mjs";
|
|
4
|
+
|
|
5
|
+
export const AGENT_IDS = Object.freeze(CODING_AGENTS.map((a) => a.id));
|
|
6
|
+
export { DEFAULT_AGENT_IDS };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {"retain" | "recall" | "reflect" | "consolidate" | "bank_scope" | "knowledge_pages" | "initiatives" | "ingest_status"} Feature
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @type {Record<string, Partial<Record<Feature, boolean>>>} */
|
|
13
|
+
export const PROVIDER_CAPABILITIES = {
|
|
14
|
+
hindsight: {
|
|
15
|
+
retain: true,
|
|
16
|
+
recall: true,
|
|
17
|
+
reflect: true,
|
|
18
|
+
consolidate: true,
|
|
19
|
+
bank_scope: true,
|
|
20
|
+
knowledge_pages: true,
|
|
21
|
+
initiatives: true,
|
|
22
|
+
ingest_status: true,
|
|
23
|
+
},
|
|
24
|
+
mem0: { retain: true, recall: true, reflect: true, consolidate: false, bank_scope: true, ingest_status: true },
|
|
25
|
+
cognee: { retain: true, recall: true, reflect: true, consolidate: true, bank_scope: true },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} provider
|
|
30
|
+
* @param {Feature} feature
|
|
31
|
+
*/
|
|
32
|
+
export function providerSupports(provider, feature) {
|
|
33
|
+
return Boolean(PROVIDER_CAPABILITIES[provider]?.[feature]);
|
|
34
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Hook side of the context store contract (docs/context-store.md). */
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/** Marker in the installed smart-compact command body; survives command expansion. */
|
|
6
|
+
export const SMART_COMPACT_MARKER = "openmem:smart-compact";
|
|
7
|
+
/** Prompt that `/smart-compact` sends in every agent (Claude Code command file, OpenCode 2 command). */
|
|
8
|
+
export const SMART_COMPACT_BODY = `<!-- ${SMART_COMPACT_MARKER} -->
|
|
9
|
+
OpenMem will trim this session's earlier context on the next request. Reply with one short line acknowledging this; do not summarize the conversation. After the trim, use \`smart_compact_search\` with this session's id to look up anything earlier.`;
|
|
10
|
+
/** Claude Code command file (`~/.claude/commands/smart-compact.md`): frontmatter plus SMART_COMPACT_BODY. */
|
|
11
|
+
export const SMART_COMPACT_COMMAND = `---
|
|
12
|
+
description: Trim this session's context on the next request (OpenMem)
|
|
13
|
+
---
|
|
14
|
+
${SMART_COMPACT_BODY}
|
|
15
|
+
`;
|
|
16
|
+
const SMART_COMPACT_PREFIX = /^\s*\/smart-compact(?:\s|$)/;
|
|
17
|
+
const SMART_COMPACT_TAG = "<command-name>/smart-compact</command-name>";
|
|
18
|
+
/** ASCII whitespace only, so the hooks and the proxy trim the same characters (session ids, OPENMEM_STATE_DIR). */
|
|
19
|
+
const ASCII_TRIM_RE = /^[ \t\n\v\f\r]+|[ \t\n\v\f\r]+$/g;
|
|
20
|
+
const SID_UNSAFE_RE = /[^A-Za-z0-9._-]/gu;
|
|
21
|
+
|
|
22
|
+
/** `$OPENMEM_STATE_DIR/context` (value trimmed), default `~/.local/share/openmem/context`. */
|
|
23
|
+
export function contextRoot(home, env = process.env) {
|
|
24
|
+
const state = String(env.OPENMEM_STATE_DIR || "").replace(ASCII_TRIM_RE, "") || path.join(home, ".local", "share", "openmem");
|
|
25
|
+
return path.join(state, "context");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Contract sanitizer (docs/context-store.md): trim surrounding ASCII whitespace, replace every character
|
|
30
|
+
* outside `[A-Za-z0-9._-]` with `_`, throw on an empty result.
|
|
31
|
+
*/
|
|
32
|
+
export function sanitizeSessionId(sessionId) {
|
|
33
|
+
const sid = String(sessionId ?? "").replace(ASCII_TRIM_RE, "").replace(SID_UNSAFE_RE, "_");
|
|
34
|
+
if (!sid) throw new Error("empty session id");
|
|
35
|
+
return sid;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sessionsDir(home, env) {
|
|
39
|
+
const root = contextRoot(home, env);
|
|
40
|
+
const dir = path.join(root, "sessions");
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
42
|
+
fs.chmodSync(root, 0o700);
|
|
43
|
+
fs.chmodSync(dir, 0o700);
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writePrivate(file, text) {
|
|
48
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
49
|
+
fs.writeFileSync(temporary, text, { mode: 0o600 });
|
|
50
|
+
fs.renameSync(temporary, file);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Write `sessions/<sid>.json`. Returns the path, or null without a session id. */
|
|
54
|
+
export function registerSession({ home, env, sessionId, agent, cwd, bank, now = new Date() }) {
|
|
55
|
+
if (!sessionId) return null;
|
|
56
|
+
const sid = sanitizeSessionId(sessionId);
|
|
57
|
+
const file = path.join(sessionsDir(home, env), `${sid}.json`);
|
|
58
|
+
const registration = {
|
|
59
|
+
session_id: sid,
|
|
60
|
+
agent,
|
|
61
|
+
cwd,
|
|
62
|
+
bank,
|
|
63
|
+
registered_at: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
64
|
+
};
|
|
65
|
+
writePrivate(file, `${JSON.stringify(registration)}\n`);
|
|
66
|
+
return file;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Write the empty `sessions/<sid>.force` flag. Returns the path, or null without a session id. */
|
|
70
|
+
export function writeForceFlag({ home, env, sessionId }) {
|
|
71
|
+
if (!sessionId) return null;
|
|
72
|
+
const file = path.join(sessionsDir(home, env), `${sanitizeSessionId(sessionId)}.force`);
|
|
73
|
+
writePrivate(file, "");
|
|
74
|
+
return file;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** True for `/smart-compact …`, Claude Code's command tag, or the expanded command body. */
|
|
78
|
+
export function isSmartCompactPrompt(prompt) {
|
|
79
|
+
const text = String(prompt || "");
|
|
80
|
+
return SMART_COMPACT_PREFIX.test(text) || text.includes(SMART_COMPACT_TAG) || text.includes(SMART_COMPACT_MARKER);
|
|
81
|
+
}
|