@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.
Files changed (230) hide show
  1. package/.agents/skills/openmem/SKILL.md +34 -0
  2. package/LICENSE +190 -0
  3. package/README.md +140 -2
  4. package/bin/hindsight-project-index +530 -0
  5. package/cli/coding-agent/access-policy.mjs +38 -0
  6. package/cli/coding-agent/agents.mjs +34 -0
  7. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  8. package/cli/coding-agent/capabilities.mjs +34 -0
  9. package/cli/coding-agent/context-store.mjs +81 -0
  10. package/cli/coding-agent/context-trim.mjs +897 -0
  11. package/cli/coding-agent/file-io.mjs +72 -0
  12. package/cli/coding-agent/hook-harnesses.mjs +88 -0
  13. package/cli/coding-agent/hook-runner.mjs +284 -0
  14. package/cli/coding-agent/install-access.mjs +119 -0
  15. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  16. package/cli/coding-agent/install-rtk.mjs +60 -0
  17. package/cli/coding-agent/install-shim.mjs +201 -0
  18. package/cli/coding-agent/opencode-config.mjs +107 -0
  19. package/cli/coding-agent/opencode-plugin.mjs +132 -0
  20. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  21. package/cli/coding-agent/recall-frame.mjs +29 -0
  22. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  23. package/cli/coding-agent/runtime.mjs +40 -0
  24. package/cli/coding-agent/session-db.mjs +626 -0
  25. package/cli/coding-agent/session-facade.mjs +86 -0
  26. package/cli/coding-agent/toml-edit.mjs +161 -0
  27. package/cli/cognee-mcp.mjs +260 -0
  28. package/cli/context-management.mjs +254 -0
  29. package/cli/feed-sync.mjs +27 -0
  30. package/cli/install-coding-agent.mjs +298 -0
  31. package/cli/install-memory-mcp.mjs +426 -0
  32. package/cli/is-main.mjs +18 -0
  33. package/cli/mcp/context-sql-runner.mjs +58 -0
  34. package/cli/mcp/context-store.mjs +670 -0
  35. package/cli/mcp/context.mjs +61 -0
  36. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  37. package/cli/mcp/export/cognee.mjs +119 -0
  38. package/cli/mcp/export/hindsight.mjs +48 -0
  39. package/cli/mcp/export/index.mjs +26 -0
  40. package/cli/mcp/export/mem0.mjs +31 -0
  41. package/cli/mcp/knowledge.mjs +333 -0
  42. package/cli/mcp/llm-synthesize.mjs +50 -0
  43. package/cli/mcp/mem0-scope-id.mjs +22 -0
  44. package/cli/mcp/memory-facade.mjs +521 -0
  45. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  46. package/cli/mcp/notebooklm-banks.mjs +21 -0
  47. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  48. package/cli/mcp/notebooklm-provider.mjs +225 -0
  49. package/cli/mcp/pmm-records.mjs +23 -0
  50. package/cli/mcp/portability.mjs +158 -0
  51. package/cli/mcp/stdio.mjs +65 -0
  52. package/cli/mcp/unified-mcp-install.mjs +65 -0
  53. package/cli/mem0-mcp.mjs +108 -0
  54. package/cli/notebooklm-mcp.mjs +236 -0
  55. package/cli/openmem-mcp.mjs +38 -0
  56. package/cli/openmem.mjs +222 -0
  57. package/cli/roots.mjs +85 -0
  58. package/cli/schedule.mjs +176 -0
  59. package/cli/stack.mjs +438 -0
  60. package/cli/switch-memory-provider.mjs +358 -0
  61. package/cli/wiring-report.mjs +125 -0
  62. package/compose.bench.yaml +12 -0
  63. package/compose.yaml +227 -0
  64. package/config/coding-agent.json +13 -0
  65. package/config/hindsight.env.example +26 -0
  66. package/config/index.yml +54 -0
  67. package/config/litellm.lemond.yaml +40 -0
  68. package/config/litellm.ollama.yaml +33 -0
  69. package/config/notebooklm.yml +33 -0
  70. package/config/openmem.toml.example +124 -0
  71. package/docs/OPENMEM.md +23 -0
  72. package/docs/architecture.md +245 -0
  73. package/docs/assets/README.md +10 -0
  74. package/docs/assets/setup-demo.gif +0 -0
  75. package/docs/coding-agent-hooks.md +75 -0
  76. package/docs/coding-agent-scopes.md +22 -0
  77. package/docs/coding-agents.md +71 -0
  78. package/docs/cognee.md +37 -0
  79. package/docs/compatibility.md +80 -0
  80. package/docs/concepts.md +135 -0
  81. package/docs/context-management.md +255 -0
  82. package/docs/context-proxy.md +115 -0
  83. package/docs/context-research.md +215 -0
  84. package/docs/context-store.md +277 -0
  85. package/docs/hindsight.md +77 -0
  86. package/docs/index-sources-roadmap.md +38 -0
  87. package/docs/index-sources.md +39 -0
  88. package/docs/indexer-sinks.md +122 -0
  89. package/docs/indexing.md +57 -0
  90. package/docs/manual-stack-setup.md +74 -0
  91. package/docs/mcp-install-guide.md +4 -0
  92. package/docs/mcp-setup.md +104 -0
  93. package/docs/mem0.md +141 -0
  94. package/docs/memory-portability.md +73 -0
  95. package/docs/memory-providers.md +246 -0
  96. package/docs/meridian-litellm.md +198 -0
  97. package/docs/model-configuration.md +106 -0
  98. package/docs/notebooklm.md +83 -0
  99. package/docs/npx-install.md +172 -0
  100. package/docs/openmem-config.md +194 -0
  101. package/docs/provider-wizard-hooks.md +222 -0
  102. package/docs/quickstart.md +107 -0
  103. package/docs/setup-wizard.md +117 -0
  104. package/docs/sources-feeds.md +39 -0
  105. package/docs/sources-readwise.md +23 -0
  106. package/docs/sources-zotero.md +22 -0
  107. package/glama.json +4 -0
  108. package/indexer/Dockerfile +38 -0
  109. package/indexer/code_outline.py +146 -0
  110. package/indexer/feed_sync.py +107 -0
  111. package/indexer/index_config.py +290 -0
  112. package/indexer/pytest.ini +5 -0
  113. package/indexer/sinks/__init__.py +148 -0
  114. package/indexer/sinks/_http.py +44 -0
  115. package/indexer/sinks/cognee.py +329 -0
  116. package/indexer/sinks/hindsight.py +107 -0
  117. package/indexer/sinks/mem0.py +247 -0
  118. package/indexer/watch.py +240 -0
  119. package/package.json +96 -4
  120. package/providers/_template/compose.yaml +52 -0
  121. package/providers/cognee/OPERATOR.md +82 -0
  122. package/providers/cognee/compose.yaml +89 -0
  123. package/providers/cognee/lib/clack.mjs +157 -0
  124. package/providers/cognee/lib/coding-agent.mjs +26 -0
  125. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  126. package/providers/cognee/lib/config.mjs +98 -0
  127. package/providers/cognee/lib/env-file.mjs +194 -0
  128. package/providers/cognee/lib/env-setup.mjs +83 -0
  129. package/providers/cognee/lib/llm-env.mjs +382 -0
  130. package/providers/cognee/lib/ports.mjs +25 -0
  131. package/providers/cognee/lib/smoke.mjs +246 -0
  132. package/providers/cognee/lib/stack-env.mjs +134 -0
  133. package/providers/cognee/lib/state.mjs +101 -0
  134. package/providers/cognee/lib/summary.mjs +56 -0
  135. package/providers/cognee/lib/validate.mjs +97 -0
  136. package/providers/cognee/manifest.json +56 -0
  137. package/providers/cognee/wizard.mjs +134 -0
  138. package/providers/graphiti/manifest.json +15 -0
  139. package/providers/hindsight/lib/clack.mjs +347 -0
  140. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  141. package/providers/hindsight/lib/config.mjs +79 -0
  142. package/providers/hindsight/lib/env-setup.mjs +72 -0
  143. package/providers/hindsight/lib/litellm-routes.mjs +196 -0
  144. package/providers/hindsight/lib/meridian.mjs +307 -0
  145. package/providers/hindsight/lib/model-backend.mjs +243 -0
  146. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  147. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  148. package/providers/hindsight/lib/ports.mjs +30 -0
  149. package/providers/hindsight/lib/probe.mjs +54 -0
  150. package/providers/hindsight/lib/stack-env.mjs +125 -0
  151. package/providers/hindsight/lib/state.mjs +80 -0
  152. package/providers/hindsight/lib/summary.mjs +57 -0
  153. package/providers/hindsight/lib/validate.mjs +81 -0
  154. package/providers/hindsight/manifest.json +58 -0
  155. package/providers/hindsight/wizard.mjs +150 -0
  156. package/providers/letta/manifest.json +29 -0
  157. package/providers/mem0/OPERATOR.md +143 -0
  158. package/providers/mem0/compose.yaml +79 -0
  159. package/providers/mem0/lib/coding-agent.mjs +58 -0
  160. package/providers/mem0/lib/config.mjs +45 -0
  161. package/providers/mem0/lib/configure.mjs +201 -0
  162. package/providers/mem0/lib/dotenv.mjs +71 -0
  163. package/providers/mem0/lib/env-file.mjs +110 -0
  164. package/providers/mem0/lib/http.mjs +69 -0
  165. package/providers/mem0/lib/litellm.mjs +457 -0
  166. package/providers/mem0/lib/mcp-install.mjs +165 -0
  167. package/providers/mem0/lib/secret.mjs +30 -0
  168. package/providers/mem0/lib/smoke.mjs +179 -0
  169. package/providers/mem0/lib/stack-env.mjs +255 -0
  170. package/providers/mem0/lib/state.mjs +67 -0
  171. package/providers/mem0/lib/summary.mjs +88 -0
  172. package/providers/mem0/lib/url.mjs +106 -0
  173. package/providers/mem0/lib/validate.mjs +158 -0
  174. package/providers/mem0/manifest.json +47 -0
  175. package/providers/mem0/wizard.mjs +146 -0
  176. package/providers/zep/manifest.json +29 -0
  177. package/proxy/config.yaml +33 -0
  178. package/proxy/openmem_context/__init__.py +6 -0
  179. package/proxy/openmem_context/adapters.py +504 -0
  180. package/proxy/openmem_context/compress.py +53 -0
  181. package/proxy/openmem_context/core.py +606 -0
  182. package/proxy/openmem_context/embed.py +212 -0
  183. package/proxy/openmem_context/recall.py +49 -0
  184. package/proxy/openmem_context/session.py +70 -0
  185. package/proxy/openmem_context/sessiondb.py +276 -0
  186. package/proxy/openmem_context/settings.py +134 -0
  187. package/proxy/openmem_context/store.py +129 -0
  188. package/proxy/openmem_context/tokens.py +123 -0
  189. package/proxy/openmem_context/ttl.py +78 -0
  190. package/proxy/openmem_context_hook.py +34 -0
  191. package/scripts/check-pack.mjs +219 -0
  192. package/scripts/gen-masks.mjs +83 -0
  193. package/scripts/gen-masks.sh +4 -0
  194. package/scripts/promotion-provider-smoke.mjs +34 -0
  195. package/scripts/run-setup.sh +30 -0
  196. package/scripts/setup.mjs +107 -0
  197. package/scripts/setup.sh +5 -0
  198. package/scripts/spin-up.sh +45 -0
  199. package/scripts/wizard.sh +5 -0
  200. package/server.json +34 -0
  201. package/sync/Dockerfile +27 -0
  202. package/sync/memory_backend.py +87 -0
  203. package/sync/notebooklm_sync.py +664 -0
  204. package/wizard/bin/pm.mjs +97 -0
  205. package/wizard/bin/setup.mjs +33 -0
  206. package/wizard/lib/compose-version.mjs +32 -0
  207. package/wizard/lib/compose.mjs +70 -0
  208. package/wizard/lib/context-step.mjs +140 -0
  209. package/wizard/lib/curl.mjs +103 -0
  210. package/wizard/lib/docker-build.mjs +148 -0
  211. package/wizard/lib/exec.mjs +41 -0
  212. package/wizard/lib/indexer.mjs +6 -0
  213. package/wizard/lib/litellm-env.mjs +55 -0
  214. package/wizard/lib/load-machine-env.mjs +4 -0
  215. package/wizard/lib/log.mjs +32 -0
  216. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  217. package/wizard/lib/notebooklm.mjs +214 -0
  218. package/wizard/lib/parse-args.mjs +87 -0
  219. package/wizard/lib/paths.mjs +39 -0
  220. package/wizard/lib/pm-config.mjs +371 -0
  221. package/wizard/lib/ports.mjs +94 -0
  222. package/wizard/lib/prompts.mjs +85 -0
  223. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  224. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  225. package/wizard/lib/providers/registry.mjs +262 -0
  226. package/wizard/lib/run-clack.mjs +108 -0
  227. package/wizard/lib/run-wizard.mjs +750 -0
  228. package/wizard/lib/validate-setup.mjs +62 -0
  229. package/wizard/lib/wizard-action.mjs +34 -0
  230. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * File helpers for the coding-agent installer: atomic rewrites of user config files
3
+ * (temp file in the same directory + rename), JSON read/write, and a silent logger.
4
+ */
5
+ import crypto from "node:crypto";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+
9
+ export const SILENT = Object.freeze({ info: () => {}, ok: () => {}, warn: () => {} });
10
+
11
+ /**
12
+ * Replace `file` with `data` atomically. A symlinked file is written through to its target.
13
+ * Mode: `opts.mode` when given (also on an existing file), else the existing file's mode,
14
+ * else the process default (0666 minus umask).
15
+ * @param {string} file
16
+ * @param {string} data
17
+ * @param {{ mode?: number }} [opts]
18
+ */
19
+ export function writeFileAtomic(file, data, opts = {}) {
20
+ let target = file;
21
+ try {
22
+ target = fs.realpathSync(file);
23
+ } catch {
24
+ // Missing file: write it where asked.
25
+ }
26
+ const dir = path.dirname(target);
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ const existing = fs.statSync(target, { throwIfNoEntry: false });
29
+ const mode = opts.mode ?? (existing ? existing.mode & 0o7777 : undefined);
30
+ const tmp = path.join(dir, `.${path.basename(target)}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`);
31
+ try {
32
+ const fd = fs.openSync(tmp, "wx", mode ?? 0o666);
33
+ try {
34
+ fs.writeFileSync(fd, data);
35
+ if (mode !== undefined) fs.fchmodSync(fd, mode);
36
+ fs.fsyncSync(fd);
37
+ } finally {
38
+ fs.closeSync(fd);
39
+ }
40
+ fs.renameSync(tmp, target);
41
+ } catch (err) {
42
+ fs.rmSync(tmp, { force: true });
43
+ throw err;
44
+ }
45
+ }
46
+
47
+ /** Parsed JSON of `file`, or `{}` when it is missing. Malformed JSON throws. */
48
+ export function readJson(file) {
49
+ return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : {};
50
+ }
51
+
52
+ /** @param {string} file @param {unknown} value @param {{ mode?: number }} [opts] */
53
+ export function writeJson(file, value, opts = {}) {
54
+ writeFileAtomic(file, `${JSON.stringify(value, null, 2)}\n`, opts);
55
+ }
56
+
57
+ /**
58
+ * Set `KEY=value` lines in a dotenv file, replacing earlier lines for those keys and keeping
59
+ * every other line. The file keeps its mode; a missing file is created with `opts.mode`.
60
+ * @param {string} file
61
+ * @param {Record<string, string>} entries
62
+ * @param {{ mode?: number }} [opts]
63
+ */
64
+ export function upsertEnvFile(file, entries, opts = {}) {
65
+ const keys = Object.keys(entries);
66
+ const exists = fs.existsSync(file);
67
+ const lines = exists ? fs.readFileSync(file, "utf8").split(/\r?\n/) : [];
68
+ const kept = lines.filter((line) => !keys.some((k) => line.startsWith(`${k}=`)));
69
+ while (kept.length && kept[kept.length - 1] === "") kept.pop();
70
+ const text = `${[...kept, ...keys.map((k) => `${k}=${entries[k]}`)].join("\n")}\n`;
71
+ writeFileAtomic(file, text, exists ? {} : opts);
72
+ }
@@ -0,0 +1,88 @@
1
+ /** Hook event names and stdout shapes per coding-agent host. */
2
+
3
+ export const SHIM_MARKER = "coding-agent/hook-runner.mjs";
4
+
5
+ export const SHIM_HARNESSES = {
6
+ "claude-code": {
7
+ configStyle: "nested",
8
+ settingsPath: (home) => `${home}/.claude/settings.json`,
9
+ rootKey: "hooks",
10
+ events: {
11
+ sessionStart: { hook: "SessionStart", timeout: 30 },
12
+ prompt: { hook: "UserPromptSubmit", timeout: 30 },
13
+ stop: { hook: "Stop", timeout: 60 },
14
+ },
15
+ parse: {
16
+ sessionStart: (ev) => ({ cwd: ev.cwd, sessionId: ev.session_id }),
17
+ prompt: (ev) => ({ cwd: ev.cwd, sessionId: ev.session_id, prompt: ev.prompt }),
18
+ stop: (ev) => ({ cwd: ev.cwd, sessionId: ev.session_id, transcriptPath: ev.transcript_path }),
19
+ },
20
+ emit: {
21
+ sessionStart: (ctx) => ({
22
+ hookSpecificOutput: { hookEventName: "SessionStart", ...(ctx ? { additionalContext: ctx } : {}) },
23
+ }),
24
+ prompt: (ctx) => ({
25
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", ...(ctx ? { additionalContext: ctx } : {}) },
26
+ }),
27
+ stop: () => ({}),
28
+ },
29
+ },
30
+ codex: {
31
+ configStyle: "nested",
32
+ settingsPath: (home) => `${home}/.codex/hooks.json`,
33
+ rootKey: "hooks",
34
+ events: {
35
+ sessionStart: { hook: "SessionStart", timeout: 30 },
36
+ prompt: { hook: "UserPromptSubmit", timeout: 30 },
37
+ stop: { hook: "Stop", timeout: 60 },
38
+ },
39
+ parse: {
40
+ sessionStart: (ev) => ({ cwd: ev.cwd, sessionId: ev.session_id }),
41
+ prompt: (ev) => ({ cwd: ev.cwd, sessionId: ev.session_id, prompt: ev.prompt ?? ev.user_prompt }),
42
+ stop: (ev) => ({ cwd: ev.cwd, sessionId: ev.session_id, transcriptPath: ev.transcript_path }),
43
+ },
44
+ emit: {
45
+ sessionStart: (ctx) => ({
46
+ hookSpecificOutput: { hookEventName: "SessionStart", ...(ctx ? { additionalContext: ctx } : {}) },
47
+ }),
48
+ prompt: (ctx) => ({
49
+ hookSpecificOutput: { hookEventName: "UserPromptSubmit", ...(ctx ? { additionalContext: ctx } : {}) },
50
+ }),
51
+ stop: () => ({}),
52
+ },
53
+ },
54
+ "cursor-cli": {
55
+ configStyle: "flat",
56
+ settingsPath: (home) => `${home}/.cursor/hooks.json`,
57
+ // Cursor reads `{ "version": 1, "hooks": { <event>: [{ command }] } }`.
58
+ rootKey: "hooks",
59
+ events: {
60
+ sessionStart: { hook: "sessionStart", timeout: 30 },
61
+ prompt: { hook: "beforeSubmitPrompt", timeout: 30 },
62
+ stop: { hook: "stop", timeout: 30 },
63
+ },
64
+ parse: {
65
+ sessionStart: (ev) => ({
66
+ cwd: ev.cwd ?? ev.workspace_root ?? ev.workspace_roots?.[0],
67
+ sessionId: ev.conversation_id ?? ev.session_id,
68
+ }),
69
+ prompt: (ev) => ({
70
+ cwd: ev.cwd ?? ev.workspace_root ?? ev.workspace_roots?.[0],
71
+ sessionId: ev.conversation_id ?? ev.session_id,
72
+ prompt: ev.prompt ?? ev.user_prompt,
73
+ }),
74
+ stop: (ev) => ({
75
+ cwd: ev.cwd ?? ev.workspace_root ?? ev.workspace_roots?.[0],
76
+ sessionId: ev.conversation_id ?? ev.session_id,
77
+ transcriptPath: ev.transcript_path,
78
+ }),
79
+ },
80
+ emit: {
81
+ sessionStart: (ctx) => (ctx ? { additional_context: ctx } : {}),
82
+ prompt: (ctx) => ({ continue: true, ...(ctx ? { additional_context: ctx } : {}) }),
83
+ stop: () => ({}),
84
+ },
85
+ },
86
+ };
87
+
88
+ export const SHIM_AGENT_IDS = Object.freeze(Object.keys(SHIM_HARNESSES));
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Session hook entry: node hook-runner.mjs <harness> <sessionStart|prompt|stop>
4
+ * stdin: host hook JSON. Uses unified memory facade (active MEMORY_PROVIDER).
5
+ */
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { bankFromCwd } from "./bank-from-cwd.mjs";
11
+ import { accessAllowed, readAccessConfig } from "./access-policy.mjs";
12
+ import { providerSupports } from "./capabilities.mjs";
13
+ import { isSmartCompactPrompt, registerSession, writeForceFlag } from "./context-store.mjs";
14
+ import { SHIM_HARNESSES } from "./hook-harnesses.mjs";
15
+ import { hookRecallBlock, sessionRecall, sessionRetain } from "./session-facade.mjs";
16
+ import { resolveMcpContext } from "../mcp/context.mjs";
17
+ import { resolveStackRoot } from "../stack.mjs";
18
+
19
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
20
+ function resolveAccess(cwd, config, home = process.env.HOME || os.homedir()) {
21
+ if (!accessAllowed(cwd, config)) return { allowed: false };
22
+ return { allowed: true, bank: bankFromCwd(cwd, { home, mapPathToBank: config.mapPathToBank }) };
23
+ }
24
+
25
+ async function readStdinJson() {
26
+ const chunks = [];
27
+ for await (const c of process.stdin) chunks.push(c);
28
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
29
+ if (!raw) return {};
30
+ return JSON.parse(raw);
31
+ }
32
+
33
+ const TRANSCRIPT_TAIL_BYTES = 256 * 1024;
34
+ const TRANSCRIPT_MAX_CHARS = 12000;
35
+
36
+ /** Short SessionStart banner; names knowledge tools only when the provider has them. */
37
+ export function sessionBanner(provider, bank) {
38
+ const tools = ["memory_recall", "memory_add"];
39
+ if (providerSupports(provider, "knowledge_pages")) tools.push("memory_pages_search");
40
+ if (providerSupports(provider, "initiatives")) tools.push("memory_capture_initiative");
41
+ return [
42
+ `[openmem] Memory provider ${provider}, bank ${bank}.`,
43
+ `Tools: ${tools.join(", ")}. Pass bank "${bank}" for this project.`,
44
+ ].join("\n");
45
+ }
46
+
47
+ function blockText(content, acceptedTypes) {
48
+ if (typeof content === "string") return content;
49
+ if (!Array.isArray(content)) return "";
50
+ return content
51
+ .filter((part) => part && acceptedTypes.includes(part.type) && typeof part.text === "string")
52
+ .map((part) => part.text)
53
+ .join("");
54
+ }
55
+
56
+ function safeAction(label, input) {
57
+ let parsed = input;
58
+ if (typeof input === "string") {
59
+ try { parsed = JSON.parse(input); } catch { /* retain only valid structured arguments */ }
60
+ }
61
+ const summary = parsed && typeof parsed === "object" ? JSON.stringify(parsed) : "";
62
+ return `[action: ${label}${summary ? ` ${summary.slice(0, 800)}` : ""}]`;
63
+ }
64
+
65
+ function parseTranscriptRecord(harnessId, record) {
66
+ if (!record || typeof record !== "object") return [];
67
+ const out = [];
68
+ if (harnessId === "codex") {
69
+ // Best-effort adapter for observed rollout response_item rows. Codex says
70
+ // transcript_path is provided for convenience, but its transcript schema is unstable.
71
+ const payload = record.type === "response_item" ? record.payload : null;
72
+ if (!payload || typeof payload !== "object") return out;
73
+ if (payload.type === "message" && ["user", "assistant"].includes(payload.role)) {
74
+ const accepted = payload.role === "user" ? ["input_text"] : ["output_text"];
75
+ const text = blockText(payload.content, accepted);
76
+ if (text.trim()) out.push(`${payload.role}: ${text.trim()}`);
77
+ } else if (payload.type === "function_call" || payload.type === "custom_tool_call") {
78
+ const label = payload.name || payload.type;
79
+ out.push(safeAction(label, payload.arguments ?? payload.input));
80
+ }
81
+ return out;
82
+ }
83
+ if (harnessId === "claude-code") {
84
+ // Claude transcript message rows use user/assistant roles. Keep only ordinary
85
+ // text and explicit tool-use blocks; thinking and tool-result blocks are omitted.
86
+ const message = record.message;
87
+ if (!["user", "assistant"].includes(message?.role)) return out;
88
+ const text = blockText(message.content, ["text"]);
89
+ if (text.trim()) out.push(`${message.role}: ${text.trim()}`);
90
+ if (message.role === "assistant" && Array.isArray(message.content)) {
91
+ for (const part of message.content) {
92
+ if (part?.type === "tool_use") out.push(safeAction(part.name || "tool", part.input));
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+ if (harnessId === "cursor-cli") {
98
+ // Observed Cursor agent-transcripts rows have a top-level role and a
99
+ // message.content array. This adapter is based on the local schema shape;
100
+ // live Hook transcript_path linkage remains unverified.
101
+ const role = record.role;
102
+ const message = record.message;
103
+ if (!["user", "assistant"].includes(role) || !Array.isArray(message?.content)) return out;
104
+ const text = blockText(message.content, ["text"]);
105
+ if (text.trim()) out.push(`${role}: ${text.trim()}`);
106
+ if (role === "assistant") {
107
+ for (const part of message.content) {
108
+ if (part?.type === "tool_use") out.push(safeAction(part.name || "tool", part.input));
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+ return out;
114
+ }
115
+
116
+ function cursorFile(home) {
117
+ return path.join(home, ".config", "personal-memory", "transcript-cursors.json");
118
+ }
119
+
120
+ function loadTranscriptCursors(home) {
121
+ try {
122
+ const value = JSON.parse(fs.readFileSync(cursorFile(home), "utf8"));
123
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
124
+ } catch { return {}; }
125
+ }
126
+
127
+ function saveTranscriptCursors(home, cursors) {
128
+ const file = cursorFile(home);
129
+ fs.mkdirSync(path.dirname(file), { recursive: true });
130
+ const temporary = `${file}.${process.pid}.tmp`;
131
+ fs.writeFileSync(temporary, JSON.stringify(cursors), { mode: 0o600 });
132
+ fs.renameSync(temporary, file);
133
+ }
134
+
135
+ function readNewTranscript(harnessId, transcriptPath, home, sessionId) {
136
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
137
+ try {
138
+ const stat = fs.statSync(transcriptPath);
139
+ if (!stat.isFile()) return null;
140
+ const key = `${harnessId}:${sessionId || path.resolve(transcriptPath)}`;
141
+ const cursors = loadTranscriptCursors(home);
142
+ const saved = cursors[key];
143
+ const oldPosition = Number.isSafeInteger(saved) ? saved
144
+ : Number.isSafeInteger(saved?.position) ? saved.position : 0;
145
+ const position = oldPosition <= stat.size ? oldPosition : 0;
146
+ let validPosition = position;
147
+ if (position && typeof saved === "object" && saved.anchor) {
148
+ const anchorLength = Math.min(256, position);
149
+ const anchorBuffer = Buffer.alloc(anchorLength);
150
+ const anchorFd = fs.openSync(transcriptPath, "r");
151
+ try { fs.readSync(anchorFd, anchorBuffer, 0, anchorLength, position - anchorLength); }
152
+ finally { fs.closeSync(anchorFd); }
153
+ if (anchorBuffer.toString("base64") !== saved.anchor) validPosition = 0;
154
+ }
155
+ const start = Math.max(validPosition, stat.size - TRANSCRIPT_TAIL_BYTES);
156
+ const length = stat.size - start;
157
+ const fd = fs.openSync(transcriptPath, "r");
158
+ const buffer = Buffer.alloc(length);
159
+ try { fs.readSync(fd, buffer, 0, length, start); } finally { fs.closeSync(fd); }
160
+ const text = buffer.toString("utf8");
161
+ const completeText = text.endsWith("\n") ? text : text.slice(0, text.lastIndexOf("\n") + 1);
162
+ const completeLines = completeText.split("\n");
163
+ if (completeLines.at(-1) === "") completeLines.pop();
164
+ if (harnessId === "cursor-cli") {
165
+ // Do not create a cursor for unknown or unverified row shapes. A single
166
+ // structurally recognized message row enables the bounded adapter.
167
+ const recognized = completeLines.some((line) => {
168
+ try {
169
+ const record = JSON.parse(line);
170
+ return ["user", "assistant"].includes(record?.role) && Array.isArray(record?.message?.content);
171
+ } catch { return false; }
172
+ });
173
+ if (!recognized) return null;
174
+ }
175
+ const extracted = [];
176
+ for (const line of completeLines) {
177
+ if (!line.trim()) continue;
178
+ try { extracted.push(...parseTranscriptRecord(harnessId, JSON.parse(line))); } catch { /* ignore malformed rows */ }
179
+ }
180
+ const content = extracted.join("\n").slice(-TRANSCRIPT_MAX_CHARS).trim();
181
+ // A bounded tail can begin mid-row. If it contains no newline, do not
182
+ // checkpoint that interior offset; retry from the last known boundary.
183
+ const nextPosition = completeText.length
184
+ ? start + Buffer.byteLength(completeText)
185
+ : validPosition;
186
+ const anchorLength = Math.min(256, nextPosition);
187
+ const anchorBuffer = Buffer.alloc(anchorLength);
188
+ const anchorFd = fs.openSync(transcriptPath, "r");
189
+ try {
190
+ if (anchorLength) fs.readSync(anchorFd, anchorBuffer, 0, anchorLength, nextPosition - anchorLength);
191
+ } finally { fs.closeSync(anchorFd); }
192
+ return {
193
+ key, cursors, position: { position: nextPosition, anchor: anchorBuffer.toString("base64") }, content,
194
+ };
195
+ } catch { return null; }
196
+ }
197
+
198
+ export async function runHook(harnessId, phase, { home, env, repoRoot, ev, fetchFn = fetch }) {
199
+ const spec = SHIM_HARNESSES[harnessId];
200
+ if (!spec) throw new Error(`unknown harness ${harnessId}`);
201
+ const parsed = spec.parse[phase](ev);
202
+ const smartCompact = phase === "prompt" && isSmartCompactPrompt(parsed.prompt);
203
+ if (smartCompact) {
204
+ // Context trimming is independent of memory access; the flag never fails the hook.
205
+ try { writeForceFlag({ home, env, sessionId: parsed.sessionId }); } catch { /* best effort */ }
206
+ }
207
+ const config = readAccessConfig(home);
208
+ const access = config ? resolveAccess(parsed.cwd, config, home) : { allowed: false };
209
+ if (!access.allowed) return spec.emit[phase](undefined);
210
+ const bank = access.bank;
211
+ let runtimeConfig = {};
212
+ try {
213
+ runtimeConfig = JSON.parse(fs.readFileSync(path.join(repoRoot, "agent-runtime.json"), "utf8"));
214
+ } catch {
215
+ // Checkout and direct test calls use ordinary stack resolution.
216
+ }
217
+ const runtimeEnv = { ...env, PM_STACK_ROOT: env.PM_STACK_ROOT || runtimeConfig.stackRoot };
218
+ const stack = resolveStackRoot({ pkgRoot: repoRoot, env: runtimeEnv, home });
219
+ const ctx = resolveMcpContext({ home, stackRoot: stack.root, repoRoot, env: runtimeEnv });
220
+ let context = "";
221
+
222
+ if (phase === "sessionStart") {
223
+ try {
224
+ registerSession({ home, env, sessionId: parsed.sessionId, agent: harnessId, cwd: parsed.cwd, bank });
225
+ } catch { /* Registration is optional for the proxy; never fail SessionStart. */ }
226
+ const recalled = await sessionRecall(
227
+ ctx,
228
+ { query: "What should I know about this project and recent decisions?", bank, limit: 8,
229
+ budget: "low", maxTokens: 1200 },
230
+ fetchFn,
231
+ );
232
+ const banner = sessionBanner(ctx.provider, bank);
233
+ const block = hookRecallBlock(recalled);
234
+ context = block ? `${banner}\n\n${block}` : banner;
235
+ } else if (phase === "prompt") {
236
+ const q = String(parsed.prompt || "").trim();
237
+ if (q && !smartCompact) {
238
+ context = hookRecallBlock(await sessionRecall(ctx, { query: q, bank, limit: 8 }, fetchFn));
239
+ }
240
+ } else if (phase === "stop") {
241
+ const transcript = readNewTranscript(harnessId, parsed.transcriptPath, home, parsed.sessionId);
242
+ if (transcript) {
243
+ if (transcript.content) {
244
+ await sessionRetain(
245
+ ctx,
246
+ {
247
+ bank,
248
+ content: `Session turn (${harnessId} ${parsed.sessionId || "unknown"}):\n${transcript.content}`,
249
+ },
250
+ fetchFn,
251
+ );
252
+ }
253
+ transcript.cursors[transcript.key] = transcript.position;
254
+ saveTranscriptCursors(home, transcript.cursors);
255
+ }
256
+ return spec.emit.stop();
257
+ }
258
+
259
+ return spec.emit[phase](context?.trim() || undefined);
260
+ }
261
+
262
+ async function main() {
263
+ const [harnessId, phase] = process.argv.slice(2);
264
+ if (!harnessId || !phase || !SHIM_HARNESSES[harnessId]?.parse[phase]) {
265
+ console.error("usage: hook-runner.mjs <claude-code|codex|cursor-cli> <sessionStart|prompt|stop>");
266
+ process.exit(2);
267
+ }
268
+ try {
269
+ const ev = await readStdinJson();
270
+ const out = await runHook(harnessId, phase, {
271
+ home: process.env.HOME || os.homedir(),
272
+ env: process.env,
273
+ repoRoot: PKG_ROOT,
274
+ ev,
275
+ });
276
+ if (out && Object.keys(out).length) process.stdout.write(`${JSON.stringify(out)}\n`);
277
+ } catch (err) {
278
+ console.error(`openmem hook: ${err instanceof Error ? err.message : err}`);
279
+ process.exit(1);
280
+ }
281
+ }
282
+
283
+ const self = fileURLToPath(import.meta.url);
284
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(self)) main();
@@ -0,0 +1,119 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import TOML from "@iarna/toml";
4
+ import { readJson, writeFileAtomic, writeJson } from "./file-io.mjs";
5
+ import { mergeOpencodeConfig, opencodeConfigPath } from "./opencode-config.mjs";
6
+ import { removeSpans, tomlSpansUnder, tomlStatements } from "./toml-edit.mjs";
7
+
8
+ const SKILL_TARGETS = {
9
+ "claude-code": ".claude/skills/openmem",
10
+ codex: ".agents/skills/openmem",
11
+ "cursor-cli": ".cursor/skills/openmem",
12
+ opencode: ".config/opencode/skills/openmem",
13
+ };
14
+
15
+ /** Set `[table] key = true` unless the file sets it (to any value) already. */
16
+ function enableTomlFlag(text, table, key) {
17
+ const statements = tomlStatements(text);
18
+ if (statements.some((s) => s.kind === "kv" && s.path.length === 2 && s.path[0] === table && s.path[1] === key)) return text;
19
+ const at = statements.findIndex((s) => s.kind === "table" && s.path.length === 1 && s.path[0] === table);
20
+ if (at < 0) return `${text}${text && !text.endsWith("\n") ? "\n" : ""}[${table}]\n${key} = true\n`;
21
+ let end = statements[at].end;
22
+ for (let k = at + 1; k < statements.length && statements[k].kind === "kv"; k++) end = statements[k].end;
23
+ const head = text.slice(0, end);
24
+ return `${head}${head.endsWith("\n") ? "" : "\n"}${key} = true\n${text.slice(end)}`;
25
+ }
26
+
27
+ /** Add top-level `key = value` (before the first table) unless the file sets `key` already. */
28
+ function setTomlDefault(text, key, value) {
29
+ const statements = tomlStatements(text);
30
+ if (statements.some((s) => s.kind === "kv" && s.path.length === 1 && s.path[0] === key)) return text;
31
+ const firstTable = statements.findIndex((s) => s.kind !== "kv");
32
+ const at = firstTable < 0 ? text.length : statements[firstTable].start;
33
+ const head = text.slice(0, at);
34
+ return `${head}${head && !head.endsWith("\n") ? "\n" : ""}${key} = ${JSON.stringify(value)}\n${text.slice(at)}`;
35
+ }
36
+
37
+ /**
38
+ * Codex config with hooks enabled, the workspace-write sandbox with network allowed (Codex defaults to a
39
+ * read-only sandbox without network, so curl and package installs fail) and exactly one current
40
+ * `[mcp_servers.openmem]`; throws on a TOML it cannot edit.
41
+ */
42
+ function updateCodexToml(text, scriptPath, stackRoot) {
43
+ text = setTomlDefault(text, "sandbox_mode", "workspace-write");
44
+ text = enableTomlFlag(text, "features", "hooks");
45
+ text = enableTomlFlag(text, "sandbox_workspace_write", "network_access");
46
+ const section = [
47
+ "[mcp_servers.openmem]",
48
+ 'command = "node"',
49
+ `args = [${JSON.stringify(scriptPath)}]`,
50
+ "[mcp_servers.openmem.env]",
51
+ `PM_STACK_ROOT = ${JSON.stringify(stackRoot)}`,
52
+ ].join("\n");
53
+ // Drop current openmem blocks and the pre-rebrand personal-memory name.
54
+ const spans = ["openmem", "personal-memory"].flatMap((name) => tomlSpansUnder(text, ["mcp_servers", name]));
55
+ const retained = removeSpans(text, spans).trimEnd();
56
+ const out = `${retained ? `${retained}\n\n` : ""}${section}\n`;
57
+ TOML.parse(out);
58
+ return out;
59
+ }
60
+
61
+ /** Install one shared skill and the unified MCP in each selected agent's global config. */
62
+ export function installAgentAccess({ home, runtimeRoot, stackRoot, agents, dry = false, h }) {
63
+ const scriptPath = path.join(runtimeRoot, "cli", "openmem-mcp.mjs");
64
+ const skillSource = path.join(runtimeRoot, ".agents", "skills", "openmem", "SKILL.md");
65
+ for (const agent of agents) {
66
+ const skillRel = SKILL_TARGETS[agent];
67
+ if (!skillRel) continue;
68
+ const skillPath = path.join(home, skillRel, "SKILL.md");
69
+ if (dry) h?.info(`[dry-run] install ${agent} openmem skill → ${skillPath}`);
70
+ else {
71
+ fs.mkdirSync(path.dirname(skillPath), { recursive: true });
72
+ fs.copyFileSync(skillSource, skillPath);
73
+ h?.ok(`${agent} skill → ${skillPath}`);
74
+ }
75
+ if (agent === "cursor-cli") continue; // install-mcp owns ~/.cursor/mcp.json
76
+ if (agent === "codex") {
77
+ const file = path.join(home, ".codex", "config.toml");
78
+ if (dry) h?.info(`[dry-run] merge openmem MCP into ${file}`);
79
+ else {
80
+ const current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
81
+ let next;
82
+ try {
83
+ next = updateCodexToml(current, scriptPath, stackRoot);
84
+ } catch (err) {
85
+ h?.warn(`codex: left ${file} unchanged; add [mcp_servers.openmem] by hand (${err.message})`);
86
+ continue;
87
+ }
88
+ writeFileAtomic(file, next);
89
+ h?.ok(`codex MCP → ${file}`);
90
+ }
91
+ continue;
92
+ }
93
+ const file = agent === "claude-code" ? path.join(home, ".claude.json") : opencodeConfigPath(home);
94
+ if (dry) {
95
+ h?.info(`[dry-run] merge openmem MCP into ${file}`);
96
+ continue;
97
+ }
98
+ if (agent === "claude-code") {
99
+ const config = readJson(file);
100
+ const servers = { ...(config.mcpServers || {}) };
101
+ delete servers["personal-memory"];
102
+ servers.openmem = {
103
+ type: "stdio", command: "node", args: [scriptPath], env: { PM_STACK_ROOT: stackRoot },
104
+ };
105
+ config.mcpServers = servers;
106
+ writeJson(file, config);
107
+ } else {
108
+ mergeOpencodeConfig(home, "mcp", (existing) => {
109
+ const next = { ...existing, openmem: {
110
+ type: "local", command: ["node", scriptPath], enabled: true,
111
+ environment: { PM_STACK_ROOT: stackRoot },
112
+ } };
113
+ delete next["personal-memory"];
114
+ return next;
115
+ });
116
+ }
117
+ h?.ok(`${agent} MCP → ${file}`);
118
+ }
119
+ }