@bcmyguest/openmem 0.0.0-stage → 0.6.3

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 (191) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +136 -2
  3. package/bin/hindsight-project-index +530 -0
  4. package/cli/coding-agent/access-policy.mjs +38 -0
  5. package/cli/coding-agent/agents.mjs +34 -0
  6. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  7. package/cli/coding-agent/capabilities.mjs +23 -0
  8. package/cli/coding-agent/hook-harnesses.mjs +87 -0
  9. package/cli/coding-agent/hook-runner.mjs +260 -0
  10. package/cli/coding-agent/install-access.mjs +93 -0
  11. package/cli/coding-agent/install-shim.mjs +97 -0
  12. package/cli/coding-agent/opencode-config.mjs +91 -0
  13. package/cli/coding-agent/opencode-plugin.mjs +96 -0
  14. package/cli/coding-agent/runtime.mjs +39 -0
  15. package/cli/coding-agent/session-facade.mjs +62 -0
  16. package/cli/cognee-mcp.mjs +260 -0
  17. package/cli/feed-sync.mjs +11 -0
  18. package/cli/install-coding-agent.mjs +165 -0
  19. package/cli/install-memory-mcp.mjs +426 -0
  20. package/cli/is-main.mjs +18 -0
  21. package/cli/mcp/context.mjs +61 -0
  22. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  23. package/cli/mcp/export/cognee.mjs +119 -0
  24. package/cli/mcp/export/hindsight.mjs +48 -0
  25. package/cli/mcp/export/index.mjs +26 -0
  26. package/cli/mcp/export/mem0.mjs +31 -0
  27. package/cli/mcp/llm-synthesize.mjs +50 -0
  28. package/cli/mcp/mem0-scope-id.mjs +22 -0
  29. package/cli/mcp/memory-facade.mjs +455 -0
  30. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  31. package/cli/mcp/notebooklm-banks.mjs +21 -0
  32. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  33. package/cli/mcp/notebooklm-provider.mjs +225 -0
  34. package/cli/mcp/pmm-records.mjs +23 -0
  35. package/cli/mcp/portability.mjs +158 -0
  36. package/cli/mcp/stdio.mjs +65 -0
  37. package/cli/mcp/unified-mcp-install.mjs +65 -0
  38. package/cli/mem0-mcp.mjs +108 -0
  39. package/cli/notebooklm-mcp.mjs +236 -0
  40. package/cli/openmem-mcp.mjs +35 -0
  41. package/cli/openmem.mjs +205 -0
  42. package/cli/roots.mjs +85 -0
  43. package/cli/stack.mjs +436 -0
  44. package/cli/switch-memory-provider.mjs +358 -0
  45. package/cli/wiring-report.mjs +125 -0
  46. package/compose.bench.yaml +12 -0
  47. package/compose.yaml +164 -0
  48. package/config/coding-agent.json +13 -0
  49. package/config/hindsight.env.example +24 -0
  50. package/config/index.yml +54 -0
  51. package/config/litellm.lemond.yaml +40 -0
  52. package/config/litellm.ollama.yaml +33 -0
  53. package/config/notebooklm.yml +33 -0
  54. package/config/openmem.toml.example +62 -0
  55. package/docs/OPENMEM.md +23 -0
  56. package/docs/architecture.md +219 -0
  57. package/docs/assets/README.md +10 -0
  58. package/docs/assets/setup-demo.gif +0 -0
  59. package/docs/coding-agent-hooks.md +51 -0
  60. package/docs/coding-agent-scopes.md +22 -0
  61. package/docs/coding-agents.md +51 -0
  62. package/docs/cognee.md +37 -0
  63. package/docs/concepts.md +112 -0
  64. package/docs/hindsight.md +74 -0
  65. package/docs/index-sources-roadmap.md +38 -0
  66. package/docs/index-sources.md +39 -0
  67. package/docs/indexer-sinks.md +122 -0
  68. package/docs/indexing.md +57 -0
  69. package/docs/manual-stack-setup.md +74 -0
  70. package/docs/mcp-install-guide.md +4 -0
  71. package/docs/mcp-setup.md +101 -0
  72. package/docs/mem0.md +103 -0
  73. package/docs/memory-portability.md +73 -0
  74. package/docs/memory-providers.md +246 -0
  75. package/docs/model-configuration.md +84 -0
  76. package/docs/notebooklm.md +83 -0
  77. package/docs/npx-install.md +170 -0
  78. package/docs/openmem-config.md +194 -0
  79. package/docs/provider-wizard-hooks.md +222 -0
  80. package/docs/quickstart.md +101 -0
  81. package/docs/setup-wizard.md +116 -0
  82. package/docs/sources-feeds.md +52 -0
  83. package/docs/sources-readwise.md +23 -0
  84. package/docs/sources-zotero.md +22 -0
  85. package/glama.json +4 -0
  86. package/indexer/Dockerfile +38 -0
  87. package/indexer/code_outline.py +146 -0
  88. package/indexer/feed_sync.py +107 -0
  89. package/indexer/index_config.py +290 -0
  90. package/indexer/pytest.ini +5 -0
  91. package/indexer/sinks/__init__.py +148 -0
  92. package/indexer/sinks/_http.py +44 -0
  93. package/indexer/sinks/cognee.py +329 -0
  94. package/indexer/sinks/hindsight.py +107 -0
  95. package/indexer/sinks/mem0.py +247 -0
  96. package/indexer/watch.py +240 -0
  97. package/package.json +95 -4
  98. package/providers/_template/compose.yaml +52 -0
  99. package/providers/cognee/OPERATOR.md +82 -0
  100. package/providers/cognee/compose.yaml +89 -0
  101. package/providers/cognee/lib/clack.mjs +157 -0
  102. package/providers/cognee/lib/coding-agent.mjs +26 -0
  103. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  104. package/providers/cognee/lib/config.mjs +98 -0
  105. package/providers/cognee/lib/env-file.mjs +194 -0
  106. package/providers/cognee/lib/env-setup.mjs +83 -0
  107. package/providers/cognee/lib/llm-env.mjs +382 -0
  108. package/providers/cognee/lib/ports.mjs +25 -0
  109. package/providers/cognee/lib/smoke.mjs +246 -0
  110. package/providers/cognee/lib/stack-env.mjs +134 -0
  111. package/providers/cognee/lib/state.mjs +101 -0
  112. package/providers/cognee/lib/summary.mjs +56 -0
  113. package/providers/cognee/lib/validate.mjs +97 -0
  114. package/providers/cognee/manifest.json +56 -0
  115. package/providers/cognee/wizard.mjs +134 -0
  116. package/providers/graphiti/manifest.json +15 -0
  117. package/providers/hindsight/lib/clack.mjs +245 -0
  118. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  119. package/providers/hindsight/lib/config.mjs +73 -0
  120. package/providers/hindsight/lib/env-setup.mjs +71 -0
  121. package/providers/hindsight/lib/litellm-routes.mjs +187 -0
  122. package/providers/hindsight/lib/model-backend.mjs +214 -0
  123. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  124. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  125. package/providers/hindsight/lib/ports.mjs +30 -0
  126. package/providers/hindsight/lib/probe.mjs +33 -0
  127. package/providers/hindsight/lib/stack-env.mjs +132 -0
  128. package/providers/hindsight/lib/state.mjs +75 -0
  129. package/providers/hindsight/lib/summary.mjs +43 -0
  130. package/providers/hindsight/lib/validate.mjs +75 -0
  131. package/providers/hindsight/manifest.json +58 -0
  132. package/providers/hindsight/wizard.mjs +149 -0
  133. package/providers/letta/manifest.json +29 -0
  134. package/providers/mem0/OPERATOR.md +124 -0
  135. package/providers/mem0/compose.yaml +57 -0
  136. package/providers/mem0/lib/coding-agent.mjs +58 -0
  137. package/providers/mem0/lib/config.mjs +45 -0
  138. package/providers/mem0/lib/configure.mjs +135 -0
  139. package/providers/mem0/lib/dotenv.mjs +71 -0
  140. package/providers/mem0/lib/env-file.mjs +110 -0
  141. package/providers/mem0/lib/http.mjs +69 -0
  142. package/providers/mem0/lib/mcp-install.mjs +165 -0
  143. package/providers/mem0/lib/secret.mjs +30 -0
  144. package/providers/mem0/lib/smoke.mjs +179 -0
  145. package/providers/mem0/lib/stack-env.mjs +216 -0
  146. package/providers/mem0/lib/state.mjs +67 -0
  147. package/providers/mem0/lib/summary.mjs +60 -0
  148. package/providers/mem0/lib/url.mjs +106 -0
  149. package/providers/mem0/lib/validate.mjs +158 -0
  150. package/providers/mem0/manifest.json +47 -0
  151. package/providers/mem0/wizard.mjs +133 -0
  152. package/providers/zep/manifest.json +29 -0
  153. package/scripts/check-pack.mjs +218 -0
  154. package/scripts/gen-masks.mjs +83 -0
  155. package/scripts/gen-masks.sh +4 -0
  156. package/scripts/promotion-provider-smoke.mjs +34 -0
  157. package/scripts/run-setup.sh +30 -0
  158. package/scripts/setup.mjs +94 -0
  159. package/scripts/setup.sh +5 -0
  160. package/scripts/spin-up.sh +45 -0
  161. package/scripts/wizard.sh +5 -0
  162. package/server.json +34 -0
  163. package/skills/openmem/SKILL.md +29 -0
  164. package/sync/Dockerfile +27 -0
  165. package/sync/memory_backend.py +87 -0
  166. package/sync/notebooklm_sync.py +664 -0
  167. package/wizard/bin/pm.mjs +97 -0
  168. package/wizard/bin/setup.mjs +33 -0
  169. package/wizard/lib/compose-version.mjs +32 -0
  170. package/wizard/lib/compose.mjs +70 -0
  171. package/wizard/lib/curl.mjs +103 -0
  172. package/wizard/lib/docker-build.mjs +148 -0
  173. package/wizard/lib/exec.mjs +41 -0
  174. package/wizard/lib/indexer.mjs +6 -0
  175. package/wizard/lib/load-machine-env.mjs +4 -0
  176. package/wizard/lib/log.mjs +32 -0
  177. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  178. package/wizard/lib/notebooklm.mjs +214 -0
  179. package/wizard/lib/parse-args.mjs +87 -0
  180. package/wizard/lib/paths.mjs +39 -0
  181. package/wizard/lib/pm-config.mjs +163 -0
  182. package/wizard/lib/ports.mjs +94 -0
  183. package/wizard/lib/prompts.mjs +57 -0
  184. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  185. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  186. package/wizard/lib/providers/registry.mjs +262 -0
  187. package/wizard/lib/run-clack.mjs +108 -0
  188. package/wizard/lib/run-wizard.mjs +755 -0
  189. package/wizard/lib/validate-setup.mjs +62 -0
  190. package/wizard/lib/wizard-action.mjs +34 -0
  191. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,101 @@
1
+ # Quickstart
2
+
3
+ Get from zero to a running stack in one wizard run. **Choose your memory provider** in the wizard (Hindsight by default; mem0 or Cognee if you prefer) — same CLI either way. Cloning the repo is optional; **`npx @bcmyguest/openmem`** is the usual path. For why OpenMem exists (interface layer, not a memory engine), see [Concepts](concepts.md).
4
+
5
+ ## Before you start
6
+
7
+ | Requirement | Why |
8
+ | --- | --- |
9
+ | Docker + Compose v2 | Runs the stack; your user must run `docker` without sudo |
10
+ | Node.js ≥ 20.12 | CLI and setup wizard |
11
+ | LLM for Hindsight (default provider) | lemond, Ollama, or OpenAI-compatible — wizard wires LiteLLM |
12
+ | git, curl | Wizard preflight |
13
+
14
+ Default stack is [Hindsight](hindsight.md). Other providers: wizard or `npx @bcmyguest/openmem providers` ([provider status](provider-status.md)).
15
+
16
+ ## 1. Run the wizard
17
+
18
+ **From npm (recommended):**
19
+
20
+ ```sh
21
+ npx @bcmyguest/openmem
22
+ ```
23
+
24
+ **From a git checkout** (repo directory is the stack root):
25
+
26
+ ```sh
27
+ git clone https://github.com/bcmyguest/openmem.git
28
+ cd openmem
29
+ npm install
30
+ npx . setup
31
+ ```
32
+
33
+ The wizard asks for a memory provider, provider settings, what to index, then starts Docker and runs a smoke test.
34
+
35
+ Useful variants:
36
+
37
+ ```sh
38
+ npx @bcmyguest/openmem setup --dry-run # plan only, no writes
39
+ npx @bcmyguest/openmem configure --yes # defaults + start, no prompts
40
+ ```
41
+
42
+ Wizard dry-run (what first setup plans — no writes):
43
+
44
+ ![Setup dry-run: npx @bcmyguest/openmem setup --dry-run](https://bcmyguest.github.io/openmem/setup-demo.gif)
45
+
46
+ ## 2. Confirm it is up
47
+
48
+ ```sh
49
+ npx @bcmyguest/openmem doctor
50
+ curl -fsS http://127.0.0.1:8888/health # Hindsight default; see wizard output for other providers
51
+ ```
52
+
53
+ ## 3. Wire your coding agent (optional)
54
+
55
+ **Session memory:** `npx @bcmyguest/openmem install-coding-agent --yes` — unified MCP **plus** session hooks for Claude Code, Codex, OpenCode, Cursor CLI (any active provider). Optional `--hindsight-plugin` adds the upstream Hindsight plugin. Not the same as `install-mcp` alone.
56
+
57
+ **`install-mcp`** merges the **`openmem` unified MCP** (`memory_add`, `memory_search`, `memory_recall`, `memory_reflect`, `memory_consolidate`, `memory_forget` on whichever provider you picked) and, when NotebookLM sync is on, the **NotebookLM MCP**:
58
+
59
+ ```sh
60
+ npx @bcmyguest/openmem install-mcp --yes
61
+ npx @bcmyguest/openmem install-mcp --yes --notebooklm # force NotebookLM entry
62
+ ```
63
+
64
+ Session hooks use the in-repo shim by default; add `--hindsight-plugin` if you also want the upstream Hindsight plugin. Optional native mem0/Cognee MCP: `install-mcp --provider-only`. Full guide: [MCP setup](mcp-setup.md).
65
+
66
+ Restart Cursor Agent after MCP changes. Move memories between providers with [Portable memory (PMM)](memory-portability.md) — `switch` alone does not migrate data.
67
+
68
+ ## Day-2
69
+
70
+ ```sh
71
+ npx @bcmyguest/openmem up
72
+ npx @bcmyguest/openmem down
73
+ npx @bcmyguest/openmem switch mem0 --yes
74
+ npx @bcmyguest/openmem doctor
75
+ ```
76
+
77
+ | Command | Purpose |
78
+ | --- | --- |
79
+ | `up` / `down` | Start or stop the stack (down keeps data volumes) |
80
+ | `switch <id> --yes` | Change provider (no memory migration) |
81
+ | `doctor` | Validate config without the smoke test |
82
+
83
+ In a checkout, use `npx .` instead of `npx @bcmyguest/openmem`.
84
+
85
+ ## Where files live
86
+
87
+ | Install | Stack root (compose, `.env`) |
88
+ | --- | --- |
89
+ | `npx @bcmyguest/openmem` | `~/.local/share/personal-memory/stack` (override: `PM_STACK_ROOT`) |
90
+ | Git checkout | The clone directory |
91
+
92
+ Secrets and index config: `~/.config/hindsight/` (and optional `~/.config/personal-memory/config.toml`).
93
+
94
+ ## Next
95
+
96
+ - [MCP setup](mcp-setup.md) — unified `memory_*`, provider-native, NotebookLM
97
+ - [Portable memory (PMM)](memory-portability.md) — export/import
98
+ - [Architecture](architecture.md) — data flow, indexing, MCP by provider
99
+ - [Index sources](index-sources.md) — git, Obsidian, notes/papers/bookmarks, feed cache
100
+ - [Install with npx](npx-install.md) — upgrades, troubleshooting
101
+ - [Configuration file](openmem-config.md) — declarative `config.toml`
@@ -0,0 +1,116 @@
1
+ # Setup wizard
2
+
3
+ The wizard is a Node program (`wizard/bin/setup.mjs`). It is exposed as:
4
+
5
+ | How you run it | When |
6
+ | --- | --- |
7
+ | `npx @bcmyguest/openmem` / `npx @bcmyguest/openmem setup` | Normal install ([Install with npx](npx-install.md)) |
8
+ | `npx . setup` / `npm run setup` / `./setup` | Git checkout (stack root stays in the repo) |
9
+
10
+ Interactive runs use [@clack/prompts](https://github.com/bombshell-dev/clack). `--yes` and `--dry-run` skip Clack.
11
+
12
+ ## Stack actions
13
+
14
+ Before provider questions, pick what the run should do (Clack or `PM_WIZARD_ACTION`):
15
+
16
+ | Action | Flag | Behavior |
17
+ | --- | --- | --- |
18
+ | Configure | default | Full wizard: provider, env, index sources, start stack, optional coding-agent step |
19
+ | Start | `--up` | Requires existing `~/.config/hindsight/wizard.conf`; starts stack and validates |
20
+ | Stop | `--down` | `docker compose down` for the project |
21
+
22
+ Non-interactive shortcuts:
23
+
24
+ ```sh
25
+ npx @bcmyguest/openmem configure --yes # setup --yes
26
+ npx @bcmyguest/openmem up # wizard --up (after first configure)
27
+ npx @bcmyguest/openmem down # wizard --down
28
+ ```
29
+
30
+ ## Wizard phases
31
+
32
+ Configure runs these steps in order (provider hooks replace or extend some steps):
33
+
34
+ | Step | Title | What happens |
35
+ | --- | --- | --- |
36
+ | 1 | Preflight | `git`, `curl`, `docker`, Compose v2; host UID/GID; API/control-plane ports |
37
+ | 2 | Memory provider | Hindsight, mem0, or Cognee ([Provider status](provider-status.md)) |
38
+ | 3 | Provider config | See table below |
39
+ | 4 | Secrets and env | Files under `~/.config/hindsight/` (mode 600); runs `scripts/setup.mjs` for stack `.env` and `compose.override.yaml` |
40
+ | 5 | Sources to index | Writes `~/.config/hindsight/index.yml`; lists sources with a dry indexer run |
41
+ | 6 | NotebookLM sync | Opt-in; Google login when enabled |
42
+ | 7 | Start the stack | Build indexer image if inputs changed; health wait; smoke retain/recall (unless `--no-smoke`) |
43
+ | 8 | Coding-agent plugin | Optional `npx` install for detected CLIs; provider-specific MCP hints |
44
+ | 9 | Validate memory setup | Provider checks + shared validation |
45
+
46
+ The last line is `wizard: changed=N` (files and containers this run touched). An unchanged re-run prints `changed=0`.
47
+
48
+ ## Step 3 by provider
49
+
50
+ | Provider | Step 3 asks for |
51
+ | --- | --- |
52
+ | Hindsight | Model backend: lemond, Ollama (host or compose profile), or custom OpenAI-compatible URL; LiteLLM routes for `memory-retain`, `memory-reflect`, `memory-consolidate` |
53
+ | mem0 | Self-hosted REST base URL and API key file path |
54
+ | Cognee | Loopback port and `LLM_API_KEY` for graph build |
55
+
56
+ Details: [Model configuration](model-configuration.md) (Hindsight), `providers/mem0/OPERATOR.md`, `providers/cognee/OPERATOR.md`.
57
+
58
+ ## Where configuration lives
59
+
60
+ | Path | Written by | Purpose |
61
+ | --- | --- | --- |
62
+ | Stack root `.env`, `.build-hash`, `compose.override.yaml` | `scripts/setup.mjs` | Compose project; indexer UID; secret-path masks |
63
+ | `~/.config/hindsight/wizard.conf` | Wizard | Provider id, backend choice, ports |
64
+ | `~/.config/hindsight/hindsight.env`, `litellm.env` | Setup / Hindsight hooks | Hindsight + LiteLLM secrets |
65
+ | `~/.config/hindsight/index.yml` | Wizard step 5 | Index roots, repos, vaults; optional `notes` / `papers` / `bookmarks` / `feeds` ([index-sources.md](index-sources.md)) |
66
+ | `~/.config/hindsight/notebooklm.yml` | Wizard step 6 | NotebookLM sync toggles |
67
+ | `~/.config/personal-memory/config.toml` | Optional; synced on configure | Declarative overrides ([Configuration file](openmem-config.md)) |
68
+ | `~/.hindsight/coding-agent.json` | Wizard / install-mcp | **Real file on disk** — do not symlink to the repo template |
69
+
70
+ ### Stack root (npx installs)
71
+
72
+ When the CLI is not running from a git checkout, mutable compose files live under:
73
+
74
+ ```text
75
+ ${PM_STACK_ROOT:-${XDG_DATA_HOME:-~/.local/share}/personal-memory/stack}
76
+ ```
77
+
78
+ Wizard code and provider hooks still run from the npm package. See [Install with npx](npx-install.md#stack-root).
79
+
80
+ ## Wizard flags
81
+
82
+ | Flag | Effect |
83
+ | --- | --- |
84
+ | `--yes` / `-y` | Accept defaults; no prompts |
85
+ | `--dry-run` | Plan only; read-only checks; no writes or containers |
86
+ | `--no-smoke` | Skip retain/recall smoke test |
87
+ | `--rebuild` | Rebuild indexer image even if sources unchanged |
88
+ | `--up` / `--down` | Start or stop only (see stack actions) |
89
+
90
+ Environment (common):
91
+
92
+ | Variable | Meaning |
93
+ | --- | --- |
94
+ | `MEMORY_PROVIDER` | Default provider id |
95
+ | `PM_BACKEND` | Hindsight backend default (`lemond`, `ollama`, `custom`) |
96
+ | `PM_PROJECT` | Docker Compose project name |
97
+ | `PM_STACK_ROOT` / `PM_USE_CHECKOUT` | Stack root location |
98
+ | `HINDSIGHT_PORT`, `HINDSIGHT_CP_PORT` | Host API and control-plane ports |
99
+
100
+ Run `./setup --help` in a checkout for the full list.
101
+
102
+ ## Indexer image builds
103
+
104
+ The wizard rebuilds the indexer image only when its inputs change (Dockerfile, `compose.yaml`, copied sources). The hash is stored in `.build-hash` at the stack root. If nothing changed, it uses `up -d --no-build` so containers are not recreated unnecessarily.
105
+
106
+ ## Related docs
107
+
108
+ | Topic | Doc |
109
+ | --- | --- |
110
+ | npx, stack root, upgrades | [npx-install.md](npx-install.md) |
111
+ | `config.toml` and day-2 tasks | [openmem-config.md](openmem-config.md) |
112
+ | Manual compose without the wizard | [manual-stack-setup.md](manual-stack-setup.md) |
113
+ | LiteLLM routes and models | [model-configuration.md](model-configuration.md) |
114
+ | Index sources and `index.yml` | [indexing.md](indexing.md) |
115
+ | NotebookLM sync and MCP | [notebooklm.md](notebooklm.md) |
116
+ | Plugins and MCP install | [coding-agents.md](coding-agents.md) |
@@ -0,0 +1,52 @@
1
+ # RSS and OpenAlex feeds
2
+
3
+ Configure `feeds` in `index.yml`, run sync, then the indexer watches `feeds.out_dir` as kind **feed** (`feed::*` banks).
4
+
5
+ ```yaml
6
+ feeds:
7
+ out_dir: ~/.local/share/personal-memory/feeds
8
+ rss:
9
+ - url: https://rss.arxiv.org/rss/cs.AI
10
+ slug: arxiv-cs-ai
11
+ openalex_queries:
12
+ - url: https://api.openalex.org/works?search=machine%20learning&per_page=25
13
+ slug: openalex-ml
14
+ ```
15
+
16
+ ```sh
17
+ npx @bcmyguest/openmem feed-sync
18
+ # checkout: npm run feed-sync
19
+ ```
20
+
21
+ Then ensure the stack indexer is running. Each run writes markdown files under `out_dir`; the watcher indexes new/changed files.
22
+
23
+ Network use: feed-sync calls public RSS/OpenAlex URLs from the host (not from CI by default).
24
+
25
+ ## Cron / timer (optional)
26
+
27
+ Run sync on a schedule so feeds stay fresh without manual commands:
28
+
29
+ ```ini
30
+ # ~/.config/systemd/user/openmem-feed-sync.timer
31
+ [Unit]
32
+ Description=Refresh openmem feed cache
33
+
34
+ [Timer]
35
+ OnCalendar=hourly
36
+ Persistent=true
37
+
38
+ [Install]
39
+ WantedBy=timers.target
40
+ ```
41
+
42
+ ```ini
43
+ # ~/.config/systemd/user/openmem-feed-sync.service
44
+ [Unit]
45
+ Description=openmem feed-sync
46
+
47
+ [Service]
48
+ Type=oneshot
49
+ ExecStart=/usr/bin/env npx @bcmyguest/openmem feed-sync
50
+ ```
51
+
52
+ Enable with `systemctl --user enable --now openmem-feed-sync.timer`.
@@ -0,0 +1,23 @@
1
+ # Readwise → indexer
2
+
3
+ Export highlights from [Readwise Reader](https://readwise.io/read) or the legacy export as **Markdown**.
4
+
5
+ 1. Export to a directory (Readwise → Export → Markdown).
6
+ 2. Add to `notes.paths` or `papers.paths` depending on how you split reading vs research:
7
+
8
+ ```yaml
9
+ notes:
10
+ paths:
11
+ - ~/Readwise/Full-Sync
12
+ ```
13
+
14
+ 3. Re-run indexing; chunks retain with bank `notes::full-sync` (slug from folder name).
15
+
16
+ Optional stable bank id:
17
+
18
+ ```yaml
19
+ banks:
20
+ ~/Readwise/Full-Sync: readwise::full-sync
21
+ ```
22
+
23
+ For automatic sync, use Readwise’s scheduled export + the same path (filesystem watcher picks up changes).
@@ -0,0 +1,22 @@
1
+ # Zotero → indexer
2
+
3
+ **Assumption:** use export-to-disk first; no Zotero API key in the stack yet.
4
+
5
+ 1. Install [Better BibTeX](https://retorque.re/zotero-better-bibtex/) (optional) for stable cite keys.
6
+ 2. Export markdown or JSON notes to a folder, e.g. `~/Zotero/export-md`.
7
+ 3. Point `papers.paths` at that folder in `~/.config/hindsight/index.yml`:
8
+
9
+ ```yaml
10
+ papers:
11
+ paths:
12
+ - ~/Zotero/export-md
13
+ ```
14
+
15
+ 4. Restart the indexer container or run `bin/hindsight-project-index --apply --papers ~/Zotero/export-md`.
16
+
17
+ Banks default to `papers::<folder-name>` unless overridden under `banks:`:
18
+
19
+ ```yaml
20
+ banks:
21
+ ~/Zotero/export-md: papers::zotero
22
+ ```
package/glama.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": ["bcmyguest"]
4
+ }
@@ -0,0 +1,38 @@
1
+ # Project indexer: one pass at startup, then re-index each source after it stops changing.
2
+ FROM python:3.12-slim
3
+
4
+ RUN apt-get update \
5
+ && apt-get install -y --no-install-recommends git ca-certificates \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ # Tree-sitter grammars are prefetched here, so the runtime never downloads and can run with no network and a
9
+ # read-only $HOME. The directory is world-readable because the container runs as the host UID.
10
+ ENV TREE_SITTER_GRAMMAR_DIR=/opt/tree-sitter-grammars
11
+ RUN pip install --no-cache-dir watchfiles==1.3.0 pyyaml==6.0.3 tree-sitter-language-pack==1.22.1 \
12
+ && python3 -c "import os; from tree_sitter_language_pack import PackConfig, download, init; \
13
+ init(PackConfig(cache_dir=os.environ['TREE_SITTER_GRAMMAR_DIR'])); \
14
+ download(['python', 'javascript', 'typescript', 'tsx', 'go', 'rust', 'java', 'c', 'cpp', 'csharp', 'ruby', 'lua', 'bash', 'php'])" \
15
+ && chmod -R a+rX /opt/tree-sitter-grammars
16
+
17
+ # The compose user is the host's UID/GID, which may have no passwd entry here. Trust every repository owner, since
18
+ # the indexer only reads them and the owner is the host user.
19
+ RUN git config --system safe.directory '*'
20
+
21
+ COPY bin/hindsight-project-index /usr/local/bin/hindsight-project-index
22
+ COPY indexer/watch.py indexer/index_config.py indexer/code_outline.py indexer/feed_sync.py /opt/indexer/
23
+ # Memory sinks (one module per provider). A new indexer/sinks/<name>.py needs no Dockerfile edit.
24
+ COPY indexer/sinks/ /opt/indexer/sinks/
25
+ # Shipped default config. index_config.py finds it at ../config/index.yml relative to itself.
26
+ COPY config/index.yml /opt/config/index.yml
27
+ RUN chmod 0755 /usr/local/bin/hindsight-project-index /opt/indexer/watch.py \
28
+ && chmod -R a+rX /opt/indexer
29
+
30
+ # $HOME is bind-mounted, so ignore the host's ~/.local site-packages (they would shadow the pinned packages).
31
+ # PYTHONPATH makes index_config, code_outline and the sinks package importable from both the indexer script and
32
+ # watch.py.
33
+ ENV PYTHONUNBUFFERED=1 \
34
+ PYTHONNOUSERSITE=1 \
35
+ PYTHONPATH=/opt/indexer \
36
+ HINDSIGHT_URL=http://hindsight:8888
37
+
38
+ CMD ["python3", "/opt/indexer/watch.py"]
@@ -0,0 +1,146 @@
1
+ """Compact symbol outlines for source files, parsed in-process with tree-sitter (tree-sitter-language-pack)."""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from tree_sitter_language_pack import PackConfig, ProcessConfig, get_parser, init, process
8
+
9
+
10
+ SIGNATURE_LIMIT = 160
11
+ DOC_LIMIT = 120
12
+ MAX_OUTLINE_LINES = 150
13
+ MAX_IMPORTS = 60
14
+ GRAMMAR_DIR_ENV = "TREE_SITTER_GRAMMAR_DIR"
15
+ LANGUAGE_BY_SUFFIX = {
16
+ ".py": "python",
17
+ ".js": "javascript",
18
+ ".jsx": "javascript",
19
+ ".ts": "typescript",
20
+ ".tsx": "tsx",
21
+ ".go": "go",
22
+ ".rs": "rust",
23
+ ".java": "java",
24
+ ".c": "c",
25
+ ".h": "c",
26
+ ".cc": "cpp",
27
+ ".cpp": "cpp",
28
+ ".hpp": "cpp",
29
+ ".cs": "csharp",
30
+ ".rb": "ruby",
31
+ ".lua": "lua",
32
+ ".sh": "bash",
33
+ ".php": "php",
34
+ }
35
+ # The pack's structure extraction does not report imports for these grammars; match their import lines instead.
36
+ FALLBACK_IMPORTS = {
37
+ "c": re.compile(r"^\s*#\s*include\b"),
38
+ "cpp": re.compile(r"^\s*#\s*include\b"),
39
+ "csharp": re.compile(r"^\s*using\s+[\w.]+\s*;"),
40
+ "ruby": re.compile(r"^\s*require(_relative)?\b"),
41
+ "lua": re.compile(r"^\s*(local\s+[\w, ]+=\s*)?require\b"),
42
+ "php": re.compile(r"^\s*use\s+[\w\\ ,{}]+;"),
43
+ }
44
+ # Decorators and attributes sit above the definition; the signature is the first line after them.
45
+ ATTRIBUTE_PREFIXES = ("@", "#[", "[")
46
+ DOC_QUOTES = ('"""', "'''")
47
+ COMMENT_MARKERS = re.compile(r"^\s*(///<|///|//!|//|/\*\*|/\*!|/\*|\*|#|--)\s*")
48
+
49
+ _ready = False
50
+
51
+
52
+ def language_for(path: str) -> str | None:
53
+ return LANGUAGE_BY_SUFFIX.get(Path(path).suffix.lower())
54
+
55
+
56
+ def _load(language: str) -> None:
57
+ """Load the grammar. Raises when it is missing, so a broken image fails the sync instead of emptying code docs."""
58
+ global _ready
59
+ if not _ready:
60
+ init(PackConfig(cache_dir=os.environ.get(GRAMMAR_DIR_ENV) or None))
61
+ _ready = True
62
+ get_parser(language)
63
+
64
+
65
+ def _clip(text: str, limit: int = SIGNATURE_LIMIT) -> str:
66
+ return text if len(text) <= limit else text[: limit - 3] + "..."
67
+
68
+
69
+ def _one_line(text: str) -> str:
70
+ return " ".join(text.split())
71
+
72
+
73
+ def _signature(item, lines: list[str]) -> str:
74
+ for row in range(item.span.start_line, min(item.span.end_line, len(lines) - 1) + 1):
75
+ text = lines[row].strip()
76
+ if text and not text.startswith(ATTRIBUTE_PREFIXES):
77
+ return _clip(text)
78
+ return _clip(f"{item.kind} {item.name or ''}".strip())
79
+
80
+
81
+ def _doc(item, lines: list[str], language: str) -> str:
82
+ if language == "python" and item.body_span is not None:
83
+ # Python docstrings are the first statement of the body; the pack does not extract them.
84
+ for row in range(item.body_span.start_line, min(item.body_span.start_line + 2, len(lines))):
85
+ text = lines[row].strip()
86
+ if not text.startswith(DOC_QUOTES):
87
+ return ""
88
+ cleaned = text[3:].rstrip("\"'").strip()
89
+ if cleaned:
90
+ return _clip(cleaned, DOC_LIMIT)
91
+ return ""
92
+ for raw in (item.doc_comment or "").splitlines():
93
+ cleaned = COMMENT_MARKERS.sub("", raw).strip().rstrip("*/").strip()
94
+ if cleaned:
95
+ return _clip(cleaned, DOC_LIMIT)
96
+ return ""
97
+
98
+
99
+ def _imports(result, language: str, lines: list[str]) -> list[str]:
100
+ found = [_clip(_one_line(item.source)) for item in result.imports]
101
+ if not found and language in FALLBACK_IMPORTS:
102
+ found = [_clip(_one_line(line)) for line in lines if FALLBACK_IMPORTS[language].match(line)]
103
+ return list(dict.fromkeys(item for item in found if item))[:MAX_IMPORTS]
104
+
105
+
106
+ def _symbol_lines(items, lines: list[str], language: str, depth: int, out: list[str], rows: set[int]) -> None:
107
+ indent = " " * depth
108
+ for item in items:
109
+ if len(out) >= MAX_OUTLINE_LINES:
110
+ return
111
+ # A one-line member shares its parent's line; listing that line twice adds nothing.
112
+ if item.span.start_line not in rows:
113
+ rows.add(item.span.start_line)
114
+ out.append(f"{indent}- L{item.span.start_line + 1} {_signature(item, lines)}")
115
+ doc = _doc(item, lines, language)
116
+ if doc:
117
+ out.append(f"{indent} doc: {doc}")
118
+ _symbol_lines(item.children, lines, language, depth + 1, out, rows)
119
+
120
+
121
+ def outline(path: str, data: bytes) -> str | None:
122
+ """Return a text outline of one source file, or None when the suffix is unsupported or the file does not parse."""
123
+ language = language_for(path)
124
+ if language is None:
125
+ return None
126
+ try:
127
+ source = data.decode("utf-8")
128
+ except UnicodeDecodeError:
129
+ return None
130
+ _load(language)
131
+ try:
132
+ result = process(source, ProcessConfig(language=language, structure=True, imports=True, exports=False))
133
+ except Exception:
134
+ return None
135
+ lines = source.split("\n")
136
+ imports = _imports(result, language, lines)
137
+ symbols: list[str] = []
138
+ _symbol_lines(result.structure, lines, language, 0, symbols, set())
139
+ if not symbols and not imports:
140
+ return None
141
+ body = [f"File: {path}", f"Language: {language}"]
142
+ if imports:
143
+ body += ["Imports:"] + [f"- {item}" for item in imports]
144
+ if symbols:
145
+ body += ["Symbols:"] + symbols
146
+ return "\n".join(body)
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch RSS and OpenAlex query results into markdown under feeds.out_dir for the indexer."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ import re
9
+ import sys
10
+ import urllib.parse
11
+ import urllib.request
12
+ import xml.etree.ElementTree as ET
13
+ from pathlib import Path
14
+
15
+ import index_config
16
+
17
+ USER_AGENT = "openmem-feed-sync/1.0"
18
+
19
+
20
+ def slugify(name: str) -> str:
21
+ return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "feed"
22
+
23
+
24
+ def fetch_bytes(url: str, timeout: int = 60) -> bytes:
25
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
26
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
27
+ return resp.read()
28
+
29
+
30
+ def write_markdown(out_dir: Path, bank_slug: str, title: str, body: str, source_url: str) -> Path:
31
+ out_dir.mkdir(parents=True, exist_ok=True)
32
+ digest = hashlib.sha256(source_url.encode()).hexdigest()[:12]
33
+ safe = slugify(title)[:60] or digest
34
+ path = out_dir / f"{bank_slug}--{safe}--{digest}.md"
35
+ text = f"---\nsource_url: {source_url}\nfeed_bank: feed::{bank_slug}\n---\n\n# {title}\n\n{body.strip()}\n"
36
+ path.write_text(text, encoding="utf-8")
37
+ return path
38
+
39
+
40
+ def sync_rss(url: str, out_dir: Path, slug: str | None = None) -> int:
41
+ bank_slug = slug or slugify(urllib.parse.urlparse(url).path or "rss")
42
+ raw = fetch_bytes(url)
43
+ root = ET.fromstring(raw)
44
+ channel = root.find("channel") or root
45
+ items = channel.findall("item") if channel is not None else root.findall(".//item")
46
+ count = 0
47
+ for item in items[:50]:
48
+ title = (item.findtext("title") or "untitled").strip()
49
+ link = (item.findtext("link") or "").strip()
50
+ desc = (item.findtext("description") or item.findtext("{http://www.w3.org/2005/Atom}summary") or "").strip()
51
+ desc = re.sub(r"<[^>]+>", " ", desc)
52
+ if not link:
53
+ continue
54
+ write_markdown(out_dir, bank_slug, title, desc or title, link)
55
+ count += 1
56
+ return count
57
+
58
+
59
+ def sync_openalex(query_url: str, out_dir: Path, slug: str | None = None) -> int:
60
+ bank_slug = slug or "openalex"
61
+ raw = fetch_bytes(query_url)
62
+ data = json.loads(raw.decode())
63
+ results = data.get("results") or []
64
+ count = 0
65
+ for work in results[:40]:
66
+ title = work.get("title") or work.get("display_name") or "work"
67
+ wid = work.get("id") or ""
68
+ abstract = work.get("abstract_inverted_index")
69
+ body = title
70
+ if isinstance(abstract, dict):
71
+ words = sorted(abstract.items(), key=lambda kv: kv[1][0] if kv[1] else 0)
72
+ body = " ".join(w for w, _ in words)
73
+ doi = (work.get("doi") or wid) or title
74
+ write_markdown(out_dir, bank_slug, title, body, str(doi))
75
+ count += 1
76
+ return count
77
+
78
+
79
+ def run(config: index_config.Config | None = None) -> int:
80
+ config = config or index_config.load()
81
+ feeds = getattr(config, "feeds_out_dir", None)
82
+ if feeds is None:
83
+ print("feeds: nothing configured (feeds.rss / feeds.openalex_queries empty)", file=sys.stderr)
84
+ return 0
85
+ out_dir = feeds
86
+ total = 0
87
+ for entry in config.feeds_rss:
88
+ url = entry if isinstance(entry, str) else entry.get("url", "")
89
+ slug = None if isinstance(entry, str) else entry.get("slug")
90
+ if not url:
91
+ continue
92
+ n = sync_rss(url, out_dir, slug)
93
+ print(f"rss {url}: {n} items -> {out_dir}")
94
+ total += n
95
+ for entry in config.feeds_openalex_queries:
96
+ url = entry if isinstance(entry, str) else entry.get("url", "")
97
+ slug = None if isinstance(entry, str) else entry.get("slug")
98
+ if not url:
99
+ continue
100
+ n = sync_openalex(url, out_dir, slug)
101
+ print(f"openalex {url}: {n} works -> {out_dir}")
102
+ total += n
103
+ return 0 if total >= 0 else 1
104
+
105
+
106
+ if __name__ == "__main__":
107
+ raise SystemExit(run())