nlm-memory 0.4.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/plugins/marketplace.json +20 -0
- package/.github/workflows/ci.yml +30 -0
- package/LICENSE +151 -0
- package/README.md +119 -0
- package/dist/cli/classify-parity.d.ts +48 -0
- package/dist/cli/classify-parity.js +182 -0
- package/dist/cli/classify-parity.js.map +1 -0
- package/dist/cli/launchctl-helpers.d.ts +26 -0
- package/dist/cli/launchctl-helpers.js +42 -0
- package/dist/cli/launchctl-helpers.js.map +1 -0
- package/dist/cli/nlm.d.ts +25 -0
- package/dist/cli/nlm.js +832 -0
- package/dist/cli/nlm.js.map +1 -0
- package/dist/core/actions/actions-log.d.ts +40 -0
- package/dist/core/actions/actions-log.js +72 -0
- package/dist/core/actions/actions-log.js.map +1 -0
- package/dist/core/actions/overlay.d.ts +30 -0
- package/dist/core/actions/overlay.js +101 -0
- package/dist/core/actions/overlay.js.map +1 -0
- package/dist/core/adapters/aider.d.ts +33 -0
- package/dist/core/adapters/aider.js +167 -0
- package/dist/core/adapters/aider.js.map +1 -0
- package/dist/core/adapters/claude-code.d.ts +32 -0
- package/dist/core/adapters/claude-code.js +270 -0
- package/dist/core/adapters/claude-code.js.map +1 -0
- package/dist/core/adapters/common.d.ts +20 -0
- package/dist/core/adapters/common.js +60 -0
- package/dist/core/adapters/common.js.map +1 -0
- package/dist/core/adapters/from-source.d.ts +11 -0
- package/dist/core/adapters/from-source.js +55 -0
- package/dist/core/adapters/from-source.js.map +1 -0
- package/dist/core/adapters/hermes-agent.d.ts +34 -0
- package/dist/core/adapters/hermes-agent.js +192 -0
- package/dist/core/adapters/hermes-agent.js.map +1 -0
- package/dist/core/adapters/hermes.d.ts +31 -0
- package/dist/core/adapters/hermes.js +247 -0
- package/dist/core/adapters/hermes.js.map +1 -0
- package/dist/core/adapters/jsonl-generic.d.ts +56 -0
- package/dist/core/adapters/jsonl-generic.js +185 -0
- package/dist/core/adapters/jsonl-generic.js.map +1 -0
- package/dist/core/adapters/opencode.d.ts +36 -0
- package/dist/core/adapters/opencode.js +213 -0
- package/dist/core/adapters/opencode.js.map +1 -0
- package/dist/core/adapters/pi.d.ts +32 -0
- package/dist/core/adapters/pi.js +233 -0
- package/dist/core/adapters/pi.js.map +1 -0
- package/dist/core/classifier/prompt.d.ts +60 -0
- package/dist/core/classifier/prompt.js +178 -0
- package/dist/core/classifier/prompt.js.map +1 -0
- package/dist/core/dataset/build-dataset.d.ts +87 -0
- package/dist/core/dataset/build-dataset.js +335 -0
- package/dist/core/dataset/build-dataset.js.map +1 -0
- package/dist/core/embedding/chunk-body.d.ts +30 -0
- package/dist/core/embedding/chunk-body.js +60 -0
- package/dist/core/embedding/chunk-body.js.map +1 -0
- package/dist/core/embedding/embed-backfill.d.ts +36 -0
- package/dist/core/embedding/embed-backfill.js +168 -0
- package/dist/core/embedding/embed-backfill.js.map +1 -0
- package/dist/core/embedding/embed-normalize.d.ts +28 -0
- package/dist/core/embedding/embed-normalize.js +98 -0
- package/dist/core/embedding/embed-normalize.js.map +1 -0
- package/dist/core/facts/backfill-facts.d.ts +58 -0
- package/dist/core/facts/backfill-facts.js +169 -0
- package/dist/core/facts/backfill-facts.js.map +1 -0
- package/dist/core/facts/extract-facts.d.ts +20 -0
- package/dist/core/facts/extract-facts.js +37 -0
- package/dist/core/facts/extract-facts.js.map +1 -0
- package/dist/core/hook/citation-detect.d.ts +32 -0
- package/dist/core/hook/citation-detect.js +105 -0
- package/dist/core/hook/citation-detect.js.map +1 -0
- package/dist/core/hook/cite-memo.d.ts +20 -0
- package/dist/core/hook/cite-memo.js +68 -0
- package/dist/core/hook/cite-memo.js.map +1 -0
- package/dist/core/hook/claude-settings.d.ts +34 -0
- package/dist/core/hook/claude-settings.js +117 -0
- package/dist/core/hook/claude-settings.js.map +1 -0
- package/dist/core/hook/gate.d.ts +11 -0
- package/dist/core/hook/gate.js +19 -0
- package/dist/core/hook/gate.js.map +1 -0
- package/dist/core/hook/hook-log.d.ts +25 -0
- package/dist/core/hook/hook-log.js +28 -0
- package/dist/core/hook/hook-log.js.map +1 -0
- package/dist/core/hook/memo-sweep.d.ts +55 -0
- package/dist/core/hook/memo-sweep.js +134 -0
- package/dist/core/hook/memo-sweep.js.map +1 -0
- package/dist/core/hook/memo.d.ts +20 -0
- package/dist/core/hook/memo.js +66 -0
- package/dist/core/hook/memo.js.map +1 -0
- package/dist/core/hook/pointer-block.d.ts +14 -0
- package/dist/core/hook/pointer-block.js +19 -0
- package/dist/core/hook/pointer-block.js.map +1 -0
- package/dist/core/hook/select.d.ts +21 -0
- package/dist/core/hook/select.js +15 -0
- package/dist/core/hook/select.js.map +1 -0
- package/dist/core/hook/transcript.d.ts +31 -0
- package/dist/core/hook/transcript.js +103 -0
- package/dist/core/hook/transcript.js.map +1 -0
- package/dist/core/ingest/ingest-session.d.ts +40 -0
- package/dist/core/ingest/ingest-session.js +71 -0
- package/dist/core/ingest/ingest-session.js.map +1 -0
- package/dist/core/providers/provider-models.d.ts +24 -0
- package/dist/core/providers/provider-models.js +72 -0
- package/dist/core/providers/provider-models.js.map +1 -0
- package/dist/core/providers/provider-registry.d.ts +62 -0
- package/dist/core/providers/provider-registry.js +143 -0
- package/dist/core/providers/provider-registry.js.map +1 -0
- package/dist/core/recall/citation-log.d.ts +28 -0
- package/dist/core/recall/citation-log.js +90 -0
- package/dist/core/recall/citation-log.js.map +1 -0
- package/dist/core/recall/filter.d.ts +11 -0
- package/dist/core/recall/filter.js +20 -0
- package/dist/core/recall/filter.js.map +1 -0
- package/dist/core/recall/index.d.ts +6 -0
- package/dist/core/recall/index.js +5 -0
- package/dist/core/recall/index.js.map +1 -0
- package/dist/core/recall/match-fields.d.ts +10 -0
- package/dist/core/recall/match-fields.js +37 -0
- package/dist/core/recall/match-fields.js.map +1 -0
- package/dist/core/recall/query-log.d.ts +36 -0
- package/dist/core/recall/query-log.js +112 -0
- package/dist/core/recall/query-log.js.map +1 -0
- package/dist/core/recall/query-shape.d.ts +22 -0
- package/dist/core/recall/query-shape.js +64 -0
- package/dist/core/recall/query-shape.js.map +1 -0
- package/dist/core/recall/recall-service.d.ts +19 -0
- package/dist/core/recall/recall-service.js +252 -0
- package/dist/core/recall/recall-service.js.map +1 -0
- package/dist/core/recall/recent-log.d.ts +16 -0
- package/dist/core/recall/recent-log.js +46 -0
- package/dist/core/recall/recent-log.js.map +1 -0
- package/dist/core/recall/tokenize.d.ts +7 -0
- package/dist/core/recall/tokenize.js +18 -0
- package/dist/core/recall/tokenize.js.map +1 -0
- package/dist/core/recall/useful-scan.d.ts +52 -0
- package/dist/core/recall/useful-scan.js +300 -0
- package/dist/core/recall/useful-scan.js.map +1 -0
- package/dist/core/recall-facts/fact-query-log.d.ts +42 -0
- package/dist/core/recall-facts/fact-query-log.js +115 -0
- package/dist/core/recall-facts/fact-query-log.js.map +1 -0
- package/dist/core/recall-facts/fact-recall-service.d.ts +34 -0
- package/dist/core/recall-facts/fact-recall-service.js +246 -0
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -0
- package/dist/core/scheduler/scan-once.d.ts +32 -0
- package/dist/core/scheduler/scan-once.js +100 -0
- package/dist/core/scheduler/scan-once.js.map +1 -0
- package/dist/core/scheduler/scheduler.d.ts +59 -0
- package/dist/core/scheduler/scheduler.js +158 -0
- package/dist/core/scheduler/scheduler.js.map +1 -0
- package/dist/core/sources/source-registry.d.ts +68 -0
- package/dist/core/sources/source-registry.js +208 -0
- package/dist/core/sources/source-registry.js.map +1 -0
- package/dist/core/storage/db-restore.d.ts +53 -0
- package/dist/core/storage/db-restore.js +113 -0
- package/dist/core/storage/db-restore.js.map +1 -0
- package/dist/core/storage/live-status.d.ts +15 -0
- package/dist/core/storage/live-status.js +43 -0
- package/dist/core/storage/live-status.js.map +1 -0
- package/dist/core/storage/migrate.d.ts +14 -0
- package/dist/core/storage/migrate.js +52 -0
- package/dist/core/storage/migrate.js.map +1 -0
- package/dist/core/storage/sqlite-fact-store.d.ts +50 -0
- package/dist/core/storage/sqlite-fact-store.js +256 -0
- package/dist/core/storage/sqlite-fact-store.js.map +1 -0
- package/dist/core/storage/sqlite-session-store.d.ts +152 -0
- package/dist/core/storage/sqlite-session-store.js +587 -0
- package/dist/core/storage/sqlite-session-store.js.map +1 -0
- package/dist/hook/pre-compact-hook.d.ts +26 -0
- package/dist/hook/pre-compact-hook.js +94 -0
- package/dist/hook/pre-compact-hook.js.map +1 -0
- package/dist/hook/prompt-recall-hook.d.ts +23 -0
- package/dist/hook/prompt-recall-hook.js +141 -0
- package/dist/hook/prompt-recall-hook.js.map +1 -0
- package/dist/hook/session-end-hook.d.ts +18 -0
- package/dist/hook/session-end-hook.js +67 -0
- package/dist/hook/session-end-hook.js.map +1 -0
- package/dist/hook/session-start-hook.d.ts +25 -0
- package/dist/hook/session-start-hook.js +129 -0
- package/dist/hook/session-start-hook.js.map +1 -0
- package/dist/hook/stop-hook.d.ts +38 -0
- package/dist/hook/stop-hook.js +171 -0
- package/dist/hook/stop-hook.js.map +1 -0
- package/dist/hook/subagent-start-hook.d.ts +30 -0
- package/dist/hook/subagent-start-hook.js +108 -0
- package/dist/hook/subagent-start-hook.js.map +1 -0
- package/dist/http/app.d.ts +65 -0
- package/dist/http/app.js +1009 -0
- package/dist/http/app.js.map +1 -0
- package/dist/install/claude-code.d.ts +57 -0
- package/dist/install/claude-code.js +76 -0
- package/dist/install/claude-code.js.map +1 -0
- package/dist/install/codex.d.ts +82 -0
- package/dist/install/codex.js +277 -0
- package/dist/install/codex.js.map +1 -0
- package/dist/install/hermes-agent.d.ts +35 -0
- package/dist/install/hermes-agent.js +48 -0
- package/dist/install/hermes-agent.js.map +1 -0
- package/dist/install/hermes.d.ts +29 -0
- package/dist/install/hermes.js +52 -0
- package/dist/install/hermes.js.map +1 -0
- package/dist/install/ollama.d.ts +54 -0
- package/dist/install/ollama.js +178 -0
- package/dist/install/ollama.js.map +1 -0
- package/dist/install/setup.d.ts +37 -0
- package/dist/install/setup.js +339 -0
- package/dist/install/setup.js.map +1 -0
- package/dist/llm/classifier-box.d.ts +29 -0
- package/dist/llm/classifier-box.js +43 -0
- package/dist/llm/classifier-box.js.map +1 -0
- package/dist/llm/deepseek-client.d.ts +40 -0
- package/dist/llm/deepseek-client.js +114 -0
- package/dist/llm/deepseek-client.js.map +1 -0
- package/dist/llm/env-autoload.d.ts +8 -0
- package/dist/llm/env-autoload.js +54 -0
- package/dist/llm/env-autoload.js.map +1 -0
- package/dist/llm/ollama-client.d.ts +47 -0
- package/dist/llm/ollama-client.js +156 -0
- package/dist/llm/ollama-client.js.map +1 -0
- package/dist/mcp/server.d.ts +64 -0
- package/dist/mcp/server.js +430 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/ports/fact-store.d.ts +82 -0
- package/dist/ports/fact-store.js +16 -0
- package/dist/ports/fact-store.js.map +1 -0
- package/dist/ports/llm-client.d.ts +42 -0
- package/dist/ports/llm-client.js +14 -0
- package/dist/ports/llm-client.js.map +1 -0
- package/dist/ports/logger.d.ts +13 -0
- package/dist/ports/logger.js +8 -0
- package/dist/ports/logger.js.map +1 -0
- package/dist/ports/session-store.d.ts +29 -0
- package/dist/ports/session-store.js +9 -0
- package/dist/ports/session-store.js.map +1 -0
- package/dist/ports/transcript-adapter.d.ts +48 -0
- package/dist/ports/transcript-adapter.js +15 -0
- package/dist/ports/transcript-adapter.js.map +1 -0
- package/dist/shared/types.d.ts +129 -0
- package/dist/shared/types.js +6 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/ui/assets/index-BA6IpU8g.css +1 -0
- package/dist/ui/assets/index-B_qIVV0k.js +69 -0
- package/dist/ui/index.html +13 -0
- package/docs/methodology/re-derivation-rate.md +112 -0
- package/docs/methodology/useful-hit-rate.md +79 -0
- package/docs/plans/2026-05-20-fts5-lexical-recall.md +1088 -0
- package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +662 -0
- package/docs/plans/2026-05-20-recall-hook-design.md +131 -0
- package/docs/plans/2026-05-20-recall-hook-implementation.md +1222 -0
- package/docs/plans/desktop-product.md +69 -0
- package/docs/plans/factstore-design.md +236 -0
- package/logs/CHANGELOG/CHANGELOG-2026.md +1389 -0
- package/logs/CHANGELOG/CHANGELOG.md +320 -0
- package/migrations/000_initial_schema.sql +174 -0
- package/migrations/001_entity_type_rename.sql +17 -0
- package/migrations/002_adapter_state_extend.sql +12 -0
- package/migrations/003_session_embeddings.sql +11 -0
- package/migrations/004_facts.sql +46 -0
- package/migrations/005_sources.sql +31 -0
- package/migrations/006_providers.sql +33 -0
- package/migrations/007_source_tokens.sql +17 -0
- package/migrations/008_fts_rebuild.sql +9 -0
- package/migrations/009_session_embedding_chunks.sql +46 -0
- package/migrations/010_sources_opencode.sql +30 -0
- package/migrations/011_sources_hermes_agent.sql +30 -0
- package/migrations/012_sources_aider.sql +30 -0
- package/migrations/013_adapter_state_failure_count.sql +12 -0
- package/package.json +56 -0
- package/plugin/.codex-plugin/plugin.json +22 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/README.md +51 -0
- package/plugin/hooks/hooks.json +25 -0
- package/plugin/scripts/prompt-recall-hook.mjs +202 -0
- package/plugin/scripts/stop-hook.mjs +306 -0
- package/plugin-hermes-agent/README.md +49 -0
- package/plugin-hermes-agent/__init__.py +75 -0
- package/plugin-hermes-agent/plugin.yaml +15 -0
- package/scripts/backfill-citations.mjs +0 -0
- package/scripts/build-codex-plugin.mjs +61 -0
- package/scripts/deepseek-probe.mjs +67 -0
- package/scripts/extract-triples.mjs +207 -0
- package/scripts/longmemeval/embedding-cache.ts +77 -0
- package/scripts/longmemeval/fetch-dataset.sh +25 -0
- package/scripts/longmemeval/run-harness.ts +315 -0
- package/scripts/longmemeval/scorer.ts +99 -0
- package/scripts/longmemeval/tsconfig.json +9 -0
- package/scripts/longmemeval/types.ts +35 -0
- package/scripts/nlm-daily-digest.py +239 -0
- package/scripts/nlm-daily-digest.sh +28 -0
- package/src/cli/classify-parity.ts +257 -0
- package/src/cli/launchctl-helpers.ts +49 -0
- package/src/cli/nlm.ts +885 -0
- package/src/core/actions/actions-log.ts +118 -0
- package/src/core/actions/overlay.ts +117 -0
- package/src/core/adapters/aider.ts +205 -0
- package/src/core/adapters/claude-code.ts +293 -0
- package/src/core/adapters/common.ts +54 -0
- package/src/core/adapters/from-source.ts +57 -0
- package/src/core/adapters/hermes-agent.ts +240 -0
- package/src/core/adapters/hermes.ts +277 -0
- package/src/core/adapters/jsonl-generic.ts +208 -0
- package/src/core/adapters/opencode.ts +281 -0
- package/src/core/adapters/pi.ts +264 -0
- package/src/core/classifier/prompt.ts +200 -0
- package/src/core/dataset/build-dataset.ts +463 -0
- package/src/core/embedding/chunk-body.ts +76 -0
- package/src/core/embedding/embed-backfill.ts +210 -0
- package/src/core/embedding/embed-normalize.ts +135 -0
- package/src/core/facts/backfill-facts.ts +254 -0
- package/src/core/facts/extract-facts.ts +50 -0
- package/src/core/hook/citation-detect.ts +124 -0
- package/src/core/hook/cite-memo.ts +68 -0
- package/src/core/hook/claude-settings.ts +166 -0
- package/src/core/hook/gate.ts +25 -0
- package/src/core/hook/hook-log.ts +41 -0
- package/src/core/hook/memo-sweep.ts +164 -0
- package/src/core/hook/memo.ts +67 -0
- package/src/core/hook/pointer-block.ts +26 -0
- package/src/core/hook/select.ts +32 -0
- package/src/core/hook/transcript.ts +121 -0
- package/src/core/ingest/ingest-session.ts +111 -0
- package/src/core/providers/provider-models.ts +100 -0
- package/src/core/providers/provider-registry.ts +196 -0
- package/src/core/recall/citation-log.ts +108 -0
- package/src/core/recall/filter.ts +27 -0
- package/src/core/recall/index.ts +6 -0
- package/src/core/recall/match-fields.ts +40 -0
- package/src/core/recall/query-log.ts +149 -0
- package/src/core/recall/query-shape.ts +66 -0
- package/src/core/recall/recall-service.ts +320 -0
- package/src/core/recall/recent-log.ts +59 -0
- package/src/core/recall/tokenize.ts +18 -0
- package/src/core/recall/useful-scan.ts +336 -0
- package/src/core/recall-facts/fact-query-log.ts +150 -0
- package/src/core/recall-facts/fact-recall-service.ts +327 -0
- package/src/core/scheduler/scan-once.ts +142 -0
- package/src/core/scheduler/scheduler.ts +225 -0
- package/src/core/sources/source-registry.ts +260 -0
- package/src/core/storage/db-restore.ts +133 -0
- package/src/core/storage/live-status.ts +45 -0
- package/src/core/storage/migrate.ts +72 -0
- package/src/core/storage/sqlite-fact-store.ts +304 -0
- package/src/core/storage/sqlite-session-store.ts +765 -0
- package/src/hook/prompt-recall-hook.ts +174 -0
- package/src/hook/session-end-hook.ts +81 -0
- package/src/hook/session-start-hook.ts +165 -0
- package/src/hook/stop-hook.ts +236 -0
- package/src/http/app.ts +1114 -0
- package/src/install/claude-code.ts +128 -0
- package/src/install/codex.ts +367 -0
- package/src/install/hermes-agent.ts +76 -0
- package/src/install/hermes.ts +78 -0
- package/src/install/ollama.ts +208 -0
- package/src/install/setup.ts +368 -0
- package/src/llm/classifier-box.ts +64 -0
- package/src/llm/deepseek-client.ts +150 -0
- package/src/llm/env-autoload.ts +55 -0
- package/src/llm/ollama-client.ts +189 -0
- package/src/mcp/server.ts +534 -0
- package/src/ports/fact-store.ts +102 -0
- package/src/ports/llm-client.ts +52 -0
- package/src/ports/logger.ts +16 -0
- package/src/ports/session-store.ts +45 -0
- package/src/ports/transcript-adapter.ts +55 -0
- package/src/shared/types.ts +145 -0
- package/src/ui/App.tsx +58 -0
- package/src/ui/components/PromoteOpenButton.tsx +65 -0
- package/src/ui/components/SessionDrawer.tsx +136 -0
- package/src/ui/components/SideNav.tsx +162 -0
- package/src/ui/components/Skeleton.tsx +107 -0
- package/src/ui/index.html +13 -0
- package/src/ui/lib/actions.ts +30 -0
- package/src/ui/lib/api.ts +92 -0
- package/src/ui/lib/dataset.ts +141 -0
- package/src/ui/lib/registries.ts +155 -0
- package/src/ui/lib/view-settings.ts +41 -0
- package/src/ui/main.tsx +15 -0
- package/src/ui/pages/Live.tsx +229 -0
- package/src/ui/pages/Pulse.tsx +415 -0
- package/src/ui/pages/Recall.tsx +190 -0
- package/src/ui/pages/River.tsx +308 -0
- package/src/ui/pages/Search.tsx +93 -0
- package/src/ui/pages/Stub.tsx +9 -0
- package/src/ui/pages/Thread.tsx +262 -0
- package/src/ui/pages/settings/Classifier.tsx +227 -0
- package/src/ui/pages/settings/Data.tsx +190 -0
- package/src/ui/pages/settings/Index.tsx +65 -0
- package/src/ui/pages/settings/Labels.tsx +224 -0
- package/src/ui/pages/settings/Providers.tsx +305 -0
- package/src/ui/pages/settings/SettingsSubnav.tsx +28 -0
- package/src/ui/pages/settings/Sources.tsx +326 -0
- package/src/ui/pages/settings/Views.tsx +96 -0
- package/src/ui/styles.css +1766 -0
- package/src/ui/tsconfig.json +21 -0
- package/src/ui/vite.config.ts +19 -0
- package/tests/fixtures/claude_code/short_session.jsonl +2 -0
- package/tests/fixtures/claude_code/standard_iso.jsonl +4 -0
- package/tests/fixtures/claude_code/tool_heavy.jsonl +8 -0
- package/tests/fixtures/claude_code/with_subagent.jsonl +7 -0
- package/tests/fixtures/facts.ts +17 -0
- package/tests/fixtures/golden-corpus.ts +85 -0
- package/tests/fixtures/hermes/paired_request_dump.json +24 -0
- package/tests/fixtures/hermes/paired_session.json +23 -0
- package/tests/fixtures/hermes/request_dump.json +28 -0
- package/tests/fixtures/hermes/session_iso.json +38 -0
- package/tests/fixtures/hermes/session_unix.json +38 -0
- package/tests/fixtures/hermes/system_only.json +18 -0
- package/tests/fixtures/pi/error-connection-abort.jsonl +8 -0
- package/tests/fixtures/pi/short-successful.jsonl +5 -0
- package/tests/fixtures/pi/with-custom-message.jsonl +6 -0
- package/tests/fixtures/sessions.ts +22 -0
- package/tests/integration/backfill-facts.test.ts +362 -0
- package/tests/integration/citation-explicit.test.ts +111 -0
- package/tests/integration/cite-event.test.ts +169 -0
- package/tests/integration/cite-memo.test.ts +87 -0
- package/tests/integration/db-restore.test.ts +153 -0
- package/tests/integration/embed-backfill.test.ts +176 -0
- package/tests/integration/fact-supersedence.test.ts +313 -0
- package/tests/integration/fts-index.test.ts +60 -0
- package/tests/integration/getbyids-sqlite.test.ts +60 -0
- package/tests/integration/hermes-agent-hooks.test.ts +248 -0
- package/tests/integration/hook-claude-settings.test.ts +205 -0
- package/tests/integration/hook-log.test.ts +54 -0
- package/tests/integration/hook-memo.test.ts +68 -0
- package/tests/integration/hook-pre-compact.test.ts +105 -0
- package/tests/integration/hook-subagent-start.test.ts +102 -0
- package/tests/integration/http.test.ts +401 -0
- package/tests/integration/keyword-search-fts.test.ts +66 -0
- package/tests/integration/mcp-recall-logging.test.ts +88 -0
- package/tests/integration/mcp.test.ts +248 -0
- package/tests/integration/memo-sweep.test.ts +91 -0
- package/tests/integration/prompt-recall-hook.test.ts +88 -0
- package/tests/integration/provider-registry.test.ts +107 -0
- package/tests/integration/recall-golden.test.ts +59 -0
- package/tests/integration/recall-sqlite.test.ts +169 -0
- package/tests/integration/scheduler.test.ts +391 -0
- package/tests/integration/session-end-hook.test.ts +48 -0
- package/tests/integration/session-start-hook.test.ts +126 -0
- package/tests/integration/source-registry.test.ts +120 -0
- package/tests/integration/sqlite-fact-store.test.ts +346 -0
- package/tests/integration/stop-hook.test.ts +560 -0
- package/tests/integration/wal-checkpoint.test.ts +49 -0
- package/tests/unit/cli/launchctl-helpers.test.ts +60 -0
- package/tests/unit/core/adapters/aider.test.ts +230 -0
- package/tests/unit/core/adapters/claude-code.test.ts +118 -0
- package/tests/unit/core/adapters/hermes-agent.test.ts +329 -0
- package/tests/unit/core/adapters/hermes.test.ts +81 -0
- package/tests/unit/core/adapters/jsonl-generic.test.ts +142 -0
- package/tests/unit/core/adapters/opencode.test.ts +354 -0
- package/tests/unit/core/adapters/pi.test.ts +110 -0
- package/tests/unit/core/classifier/prompt.test.ts +126 -0
- package/tests/unit/core/embedding/chunk-body.test.ts +100 -0
- package/tests/unit/core/facts/extract-facts.test.ts +117 -0
- package/tests/unit/core/filter.test.ts +40 -0
- package/tests/unit/core/hook/citation-detect-cite-session.test.ts +96 -0
- package/tests/unit/core/hook/citation-detect.test.ts +124 -0
- package/tests/unit/core/hook/gate.test.ts +29 -0
- package/tests/unit/core/hook/pointer-block.test.ts +22 -0
- package/tests/unit/core/hook/select.test.ts +66 -0
- package/tests/unit/core/match-fields.test.ts +39 -0
- package/tests/unit/core/mcp-cite-session.test.ts +51 -0
- package/tests/unit/core/providers/provider-models.test.ts +101 -0
- package/tests/unit/core/query-shape.test.ts +92 -0
- package/tests/unit/core/recall-facts/fact-recall-service.test.ts +258 -0
- package/tests/unit/core/recall-service.test.ts +200 -0
- package/tests/unit/core/storage/live-status.test.ts +54 -0
- package/tests/unit/core/tokenize.test.ts +32 -0
- package/tests/unit/core/useful-scan.test.ts +537 -0
- package/tests/unit/llm/embed.test.ts +93 -0
- package/tests/unit/llm/ollama-client.test.ts +124 -0
- package/tests/unit/scripts/longmemeval-scorer.test.ts +114 -0
- package/tsconfig.json +31 -0
- package/tsconfig.test.json +11 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Migration 005: sources registry.
|
|
2
|
+
--
|
|
3
|
+
-- Each row represents one transcript source the daemon should scan. The
|
|
4
|
+
-- three hardcoded adapters (claude-code / hermes / pi) become seeded rows
|
|
5
|
+
-- pointing at the same parse logic; future custom JSONL or webhook sources
|
|
6
|
+
-- live alongside them.
|
|
7
|
+
--
|
|
8
|
+
-- Parse config is a JSON blob whose shape depends on `kind`:
|
|
9
|
+
-- - "claude-code" / "hermes" / "pi": preset adapters. parse_config is
|
|
10
|
+
-- reserved but unused — paths come from path_or_url.
|
|
11
|
+
-- - "jsonl-generic": { sessionIdField, textField, startedAtField,
|
|
12
|
+
-- roleField, runtimeLabel, ... }
|
|
13
|
+
-- - "webhook": parse_config is empty; ingest is push-based.
|
|
14
|
+
--
|
|
15
|
+
-- See docs/plans/desktop-product.md (Phase 0).
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
18
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
19
|
+
kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'pi', 'jsonl-generic', 'webhook')),
|
|
20
|
+
name TEXT NOT NULL UNIQUE,
|
|
21
|
+
path_or_url TEXT,
|
|
22
|
+
runtime_label TEXT NOT NULL,
|
|
23
|
+
parse_config TEXT NOT NULL DEFAULT '{}',
|
|
24
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
|
|
30
|
+
|
|
31
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (5, '005_sources');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- Migration 006: providers registry.
|
|
2
|
+
--
|
|
3
|
+
-- Each row represents one LLM endpoint configured for classification or
|
|
4
|
+
-- (future) embedding. The classifier reads this table to choose a
|
|
5
|
+
-- provider/model at boot; the UI mutates it through /api/providers.
|
|
6
|
+
--
|
|
7
|
+
-- API key storage. v0 stores keys in the api_key column. This is fine
|
|
8
|
+
-- because the SQLite file already contains the user's transcripts —
|
|
9
|
+
-- anyone with disk access already has everything sensitive. Phase 2
|
|
10
|
+
-- (Tauri shell) migrates keys to the OS keychain and replaces the column
|
|
11
|
+
-- with a keychain reference. The API shape stays the same.
|
|
12
|
+
--
|
|
13
|
+
-- `kind` is the structural family (openai-compatible, ollama,
|
|
14
|
+
-- anthropic-native, deepseek). The classifier+model UI uses it to decide
|
|
15
|
+
-- which client class to instantiate.
|
|
16
|
+
--
|
|
17
|
+
-- See docs/plans/desktop-product.md (Phase 0 task 3).
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
kind TEXT NOT NULL CHECK (kind IN ('deepseek', 'ollama', 'openai', 'anthropic', 'openrouter', 'openai-compatible')),
|
|
22
|
+
name TEXT NOT NULL UNIQUE,
|
|
23
|
+
base_url TEXT,
|
|
24
|
+
api_key TEXT,
|
|
25
|
+
default_model TEXT,
|
|
26
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
27
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
28
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_providers_enabled ON providers(enabled) WHERE enabled = 1;
|
|
32
|
+
|
|
33
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (6, '006_providers');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Migration 007: webhook token on sources.
|
|
2
|
+
--
|
|
3
|
+
-- Webhook sources need a bearer token so external tools (anything that
|
|
4
|
+
-- pushes sessions via POST /api/ingest) can authenticate. The token is
|
|
5
|
+
-- stored alongside the source row so one webhook = one token = one
|
|
6
|
+
-- provenance label.
|
|
7
|
+
--
|
|
8
|
+
-- Storage policy mirrors providers.api_key: column in the canonical
|
|
9
|
+
-- SQLite for v0 (the DB file already holds the user's transcripts,
|
|
10
|
+
-- adding a token doesn't change the threat model). Phase 2 migrates to
|
|
11
|
+
-- OS keychain without changing the API shape.
|
|
12
|
+
--
|
|
13
|
+
-- See docs/plans/desktop-product.md (Phase 0 task 5).
|
|
14
|
+
|
|
15
|
+
ALTER TABLE sources ADD COLUMN token TEXT;
|
|
16
|
+
|
|
17
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (7, '007_source_tokens');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- One-time safety rebuild of the sessions_fts external-content FTS5 index.
|
|
2
|
+
-- The virtual table and its sync triggers (sessions_ai / sessions_au /
|
|
3
|
+
-- sessions_ad) were declared in migration 000 and have fired on every write
|
|
4
|
+
-- since, so the index is normally already in sync. This rebuild guarantees
|
|
5
|
+
-- the index matches every existing sessions row before the recall path
|
|
6
|
+
-- starts depending on FTS5 for keyword search. Safe and idempotent.
|
|
7
|
+
INSERT INTO sessions_fts(sessions_fts) VALUES('rebuild');
|
|
8
|
+
|
|
9
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (8, 'fts_rebuild');
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- Migration 009: chunk + max-pool semantic index.
|
|
2
|
+
--
|
|
3
|
+
-- Replaces session_embeddings (one vector per session, truncated at
|
|
4
|
+
-- MAX_EMBED_CHARS=8000) with per-chunk vectors. Recall-time score is
|
|
5
|
+
-- max cosine across chunks per session.
|
|
6
|
+
--
|
|
7
|
+
-- The 2026-05-25 LongMemEval-S baseline showed 98% of gold sessions
|
|
8
|
+
-- exceed 8000 chars and were silently truncated. Raising the per-call
|
|
9
|
+
-- cap (#172) hit Ollama 500s on >50% of long inputs. Chunking sidesteps
|
|
10
|
+
-- both: each chunk is well under the Ollama failure cliff, and the full
|
|
11
|
+
-- body becomes searchable. Expected lift: semantic R@5 87.2 → >92,
|
|
12
|
+
-- hybrid R@5 94.6 → >96.
|
|
13
|
+
--
|
|
14
|
+
-- Schema choices:
|
|
15
|
+
-- * Auxiliary columns (+session_id, +chunk_idx) so KNN queries return
|
|
16
|
+
-- session attribution without a join.
|
|
17
|
+
-- * Separate session_chunk_map keyed on session_id supports
|
|
18
|
+
-- `DELETE FROM session_embedding_chunks WHERE chunk_id IN
|
|
19
|
+
-- (SELECT chunk_id FROM session_chunk_map WHERE session_id = ?)`
|
|
20
|
+
-- since vec0 has no documented filtering on aux columns.
|
|
21
|
+
--
|
|
22
|
+
-- session_embeddings (single-vector) is intentionally left in place:
|
|
23
|
+
-- * keeps rollback trivial (revert recall code, old vectors still there)
|
|
24
|
+
-- * avoids forcing a multi-hour re-embed at deploy time; backfill
|
|
25
|
+
-- populates chunks asynchronously
|
|
26
|
+
-- * a future cleanup migration drops it once chunks are validated
|
|
27
|
+
|
|
28
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_embedding_chunks USING vec0(
|
|
29
|
+
chunk_id INTEGER PRIMARY KEY,
|
|
30
|
+
embedding float[768],
|
|
31
|
+
+session_id TEXT,
|
|
32
|
+
+chunk_idx INTEGER
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS session_chunk_map (
|
|
36
|
+
chunk_id INTEGER PRIMARY KEY,
|
|
37
|
+
session_id TEXT NOT NULL,
|
|
38
|
+
chunk_idx INTEGER NOT NULL,
|
|
39
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_session_chunk_map_session
|
|
43
|
+
ON session_chunk_map(session_id);
|
|
44
|
+
|
|
45
|
+
INSERT OR IGNORE INTO schema_migrations (version, name)
|
|
46
|
+
VALUES (9, '009_session_embedding_chunks');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Migration 010: add 'opencode' to the sources.kind CHECK constraint.
|
|
2
|
+
--
|
|
3
|
+
-- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
|
|
4
|
+
-- Standard approach: rename → recreate → copy → drop old.
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = OFF;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE sources_new (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
|
|
11
|
+
name TEXT NOT NULL UNIQUE,
|
|
12
|
+
path_or_url TEXT,
|
|
13
|
+
runtime_label TEXT NOT NULL,
|
|
14
|
+
parse_config TEXT NOT NULL DEFAULT '{}',
|
|
15
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
16
|
+
token TEXT,
|
|
17
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
|
|
22
|
+
|
|
23
|
+
DROP TABLE sources;
|
|
24
|
+
ALTER TABLE sources_new RENAME TO sources;
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
|
|
27
|
+
|
|
28
|
+
PRAGMA foreign_keys = ON;
|
|
29
|
+
|
|
30
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (10, '010_sources_opencode');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Migration 011: add 'hermes-agent' to the sources.kind CHECK constraint.
|
|
2
|
+
--
|
|
3
|
+
-- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
|
|
4
|
+
-- Standard approach: rename → recreate → copy → drop old.
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = OFF;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE sources_new (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'hermes-agent', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
|
|
11
|
+
name TEXT NOT NULL UNIQUE,
|
|
12
|
+
path_or_url TEXT,
|
|
13
|
+
runtime_label TEXT NOT NULL,
|
|
14
|
+
parse_config TEXT NOT NULL DEFAULT '{}',
|
|
15
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
16
|
+
token TEXT,
|
|
17
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
|
|
22
|
+
|
|
23
|
+
DROP TABLE sources;
|
|
24
|
+
ALTER TABLE sources_new RENAME TO sources;
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
|
|
27
|
+
|
|
28
|
+
PRAGMA foreign_keys = ON;
|
|
29
|
+
|
|
30
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (11, '011_sources_hermes_agent');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Migration 012: add 'aider' to the sources.kind CHECK constraint.
|
|
2
|
+
--
|
|
3
|
+
-- SQLite does not support ALTER COLUMN to modify CHECK constraints in place.
|
|
4
|
+
-- Standard approach: rename → recreate → copy → drop old.
|
|
5
|
+
|
|
6
|
+
PRAGMA foreign_keys = OFF;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE sources_new (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
kind TEXT NOT NULL CHECK (kind IN ('claude-code', 'hermes', 'hermes-agent', 'aider', 'opencode', 'pi', 'jsonl-generic', 'webhook')),
|
|
11
|
+
name TEXT NOT NULL UNIQUE,
|
|
12
|
+
path_or_url TEXT,
|
|
13
|
+
runtime_label TEXT NOT NULL,
|
|
14
|
+
parse_config TEXT NOT NULL DEFAULT '{}',
|
|
15
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
16
|
+
token TEXT,
|
|
17
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
INSERT INTO sources_new SELECT id, kind, name, path_or_url, runtime_label, parse_config, enabled, token, created_at, updated_at FROM sources;
|
|
22
|
+
|
|
23
|
+
DROP TABLE sources;
|
|
24
|
+
ALTER TABLE sources_new RENAME TO sources;
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_sources_enabled ON sources(enabled) WHERE enabled = 1;
|
|
27
|
+
|
|
28
|
+
PRAGMA foreign_keys = ON;
|
|
29
|
+
|
|
30
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (12, '012_sources_aider');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Migration 013: add failure_count to adapter_state.
|
|
2
|
+
--
|
|
3
|
+
-- Tracks consecutive classify/storage failures per source file. The scheduler
|
|
4
|
+
-- skips files whose failure_count has reached the backoff ceiling (default 3)
|
|
5
|
+
-- and whose file_size has not changed since the last attempt. When the file
|
|
6
|
+
-- grows (new content appended), failure_count resets to 0 and the file is
|
|
7
|
+
-- retried. This prevents a single permanently-broken or over-large transcript
|
|
8
|
+
-- from flooding daemon-err.log on every 30-minute tick.
|
|
9
|
+
|
|
10
|
+
ALTER TABLE adapter_state ADD COLUMN failure_count INTEGER NOT NULL DEFAULT 0;
|
|
11
|
+
|
|
12
|
+
INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (13, '013_adapter_state_failure_count');
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nlm-memory",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Local-first non-linear memory operating system for AI operators.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.0.0"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"nlm": "dist/cli/nlm.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "tsx watch src/cli/nlm.ts start",
|
|
15
|
+
"start": "node dist/cli/nlm.js start",
|
|
16
|
+
"build:server": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
|
17
|
+
"build:ui": "vite build --config src/ui/vite.config.ts",
|
|
18
|
+
"build:codex-plugin": "node scripts/build-codex-plugin.mjs",
|
|
19
|
+
"build": "npm run build:server && npm run build:ui && npm run build:codex-plugin",
|
|
20
|
+
"ui:dev": "vite --config src/ui/vite.config.ts",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"test:unit": "vitest run tests/unit",
|
|
24
|
+
"test:integration": "vitest run tests/integration",
|
|
25
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
26
|
+
"lint": "eslint src tests"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@clack/prompts": "^1.4.0",
|
|
30
|
+
"@hono/node-server": "^2.0.3",
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
+
"@toon-format/toon": "^2.3.0",
|
|
33
|
+
"better-sqlite3": "^11.5.0",
|
|
34
|
+
"commander": "^12.1.0",
|
|
35
|
+
"hono": "^4.6.0",
|
|
36
|
+
"sqlite-vec": "^0.1.6",
|
|
37
|
+
"yaml": "^2.9.0",
|
|
38
|
+
"zod": "^3.23.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
42
|
+
"@types/node": "^22.10.0",
|
|
43
|
+
"@types/react": "^18.3.28",
|
|
44
|
+
"@types/react-dom": "^18.3.7",
|
|
45
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
46
|
+
"esbuild": "^0.28.0",
|
|
47
|
+
"react": "^18.3.1",
|
|
48
|
+
"react-dom": "^18.3.1",
|
|
49
|
+
"react-router-dom": "^6.30.3",
|
|
50
|
+
"tsc-alias": "^1.8.17",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.7.0",
|
|
53
|
+
"vite": "^5.4.21",
|
|
54
|
+
"vitest": "^2.1.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nlm-memory",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Local-first persistent memory for AI coding agents. Recalls relevant prior sessions on every prompt; learns which sessions actually get cited. Owner-edited timeline beats append-only memory layers.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Edward Chalupa",
|
|
7
|
+
"url": "https://github.com/pbmagnet4"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"homepage": "https://github.com/pbmagnet4/nlm-memory-ts",
|
|
11
|
+
"repository": "https://github.com/pbmagnet4/nlm-memory-ts",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"memory",
|
|
14
|
+
"context",
|
|
15
|
+
"recall",
|
|
16
|
+
"agent",
|
|
17
|
+
"rag",
|
|
18
|
+
"session"
|
|
19
|
+
],
|
|
20
|
+
"mcpServers": "./.mcp.json",
|
|
21
|
+
"hooks": "./hooks/hooks.json"
|
|
22
|
+
}
|
package/plugin/.mcp.json
ADDED
package/plugin/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# nlm-memory Codex plugin
|
|
2
|
+
|
|
3
|
+
This directory is the Codex plugin distribution surface for nlm-memory. The
|
|
4
|
+
repo root acts as a Codex marketplace (`codex plugin marketplace add
|
|
5
|
+
pbmagnet4/nlm-memory-ts`), and this directory is the plugin Codex installs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Prerequisite: `npm install -g nlm-memory` (puts `nlm` on PATH; the MCP server
|
|
10
|
+
spawns `nlm mcp`).
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
codex plugin marketplace add pbmagnet4/nlm-memory-ts
|
|
14
|
+
codex plugin add nlm-memory@nlm-memory-ts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or use the wrapper: `nlm connect codex`.
|
|
18
|
+
|
|
19
|
+
## What ships
|
|
20
|
+
|
|
21
|
+
- **MCP server** (`.mcp.json`) — `recall_sessions`, `get_session`,
|
|
22
|
+
`recall_facts`, `get_fact_history`. Spawns `nlm mcp` over stdio.
|
|
23
|
+
- **UserPromptSubmit hook** — injects up to 3 relevant prior sessions per
|
|
24
|
+
fire, capped at 10 per conversation. Fail-open: any error yields no
|
|
25
|
+
output, never blocks a prompt.
|
|
26
|
+
- **Stop hook** — scans the assistant turn for cited session IDs and posts
|
|
27
|
+
citation events to the local daemon for learned reranking.
|
|
28
|
+
|
|
29
|
+
## What doesn't
|
|
30
|
+
|
|
31
|
+
Codex has no `SessionEnd` equivalent (CC does). The CC adapter uses
|
|
32
|
+
SessionEnd to clean up per-conversation memo files in
|
|
33
|
+
`~/.nlm/hook-state/`. The local daemon's `MemoSweepScheduler` (running every
|
|
34
|
+
5m, 24h threshold) cleans those up regardless of runtime, so the absence is
|
|
35
|
+
not a correctness issue — just a slight delay before stale memos are
|
|
36
|
+
collected.
|
|
37
|
+
|
|
38
|
+
## Hook trust
|
|
39
|
+
|
|
40
|
+
Codex requires per-hash trust before any plugin hook executes. On first
|
|
41
|
+
`codex` invocation after install, you'll be prompted to review and trust the
|
|
42
|
+
hook scripts in `scripts/`. Approve them once; trust persists per script
|
|
43
|
+
hash. Re-prompted whenever a new release ships new script hashes.
|
|
44
|
+
|
|
45
|
+
## Codex Desktop fallback
|
|
46
|
+
|
|
47
|
+
[openai/codex#16430](https://github.com/openai/codex/issues/16430) — Codex
|
|
48
|
+
Desktop builds currently do not dispatch plugin-local hooks. MCP tools still
|
|
49
|
+
work. If you're on Desktop, run `nlm connect codex --with-hooks` to write
|
|
50
|
+
absolute paths to `~/.codex/hooks.json` as a fallback. Remove the fallback
|
|
51
|
+
once #16430 ships.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"UserPromptSubmit": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-recall-hook.mjs\"",
|
|
9
|
+
"statusMessage": "nlm-memory: recalling prior sessions"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"Stop": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop-hook.mjs\""
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hook/prompt-recall-hook.ts
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
// src/core/hook/gate.ts
|
|
7
|
+
var LEADING_FILLER = /^(please|can you|could you|would you|will you|i need you to|i'd like you to|i want you to|i would like you to|help me|let's|lets|hey|ok|okay)\b[\s,]*/i;
|
|
8
|
+
var GENERATIVE_OPENER = /^(write|draft|create|compose|generate|brainstorm|design|outline|sketch|invent|rename|come up with)\b/i;
|
|
9
|
+
function classifyPrompt(prompt) {
|
|
10
|
+
let p = prompt.trim();
|
|
11
|
+
for (let i = 0; i < 3 && LEADING_FILLER.test(p); i++) {
|
|
12
|
+
p = p.replace(LEADING_FILLER, "");
|
|
13
|
+
}
|
|
14
|
+
return GENERATIVE_OPENER.test(p) ? "generative" : "evaluate";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/core/hook/hook-log.ts
|
|
18
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
function logPath() {
|
|
22
|
+
return process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
|
|
23
|
+
}
|
|
24
|
+
function appendHookLog(entry) {
|
|
25
|
+
try {
|
|
26
|
+
const path = logPath();
|
|
27
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
28
|
+
appendFileSync(path, `${JSON.stringify(entry)}
|
|
29
|
+
`, "utf8");
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/core/hook/memo.ts
|
|
35
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
36
|
+
import { homedir as homedir2 } from "node:os";
|
|
37
|
+
import { join as join2 } from "node:path";
|
|
38
|
+
function stateDir() {
|
|
39
|
+
return process.env["NLM_HOOK_STATE_DIR"] ?? join2(homedir2(), ".nlm", "hook-state");
|
|
40
|
+
}
|
|
41
|
+
function memoPath(conversationId) {
|
|
42
|
+
const safe = conversationId.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
|
|
43
|
+
return join2(stateDir(), `${safe}.json`);
|
|
44
|
+
}
|
|
45
|
+
function loadSurfaced(conversationId) {
|
|
46
|
+
try {
|
|
47
|
+
const path = memoPath(conversationId);
|
|
48
|
+
if (!existsSync(path)) return /* @__PURE__ */ new Set();
|
|
49
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
50
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
51
|
+
return new Set(parsed.filter((x) => typeof x === "string"));
|
|
52
|
+
} catch {
|
|
53
|
+
return /* @__PURE__ */ new Set();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function recordSurfaced(conversationId, ids) {
|
|
57
|
+
try {
|
|
58
|
+
const merged = loadSurfaced(conversationId);
|
|
59
|
+
for (const id of ids) merged.add(id);
|
|
60
|
+
mkdirSync2(stateDir(), { recursive: true });
|
|
61
|
+
writeFileSync(memoPath(conversationId), JSON.stringify([...merged]), "utf8");
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/core/hook/pointer-block.ts
|
|
67
|
+
function formatPointerBlock(hits) {
|
|
68
|
+
if (hits.length === 0) return "";
|
|
69
|
+
const lines = hits.map(
|
|
70
|
+
(h) => `- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`
|
|
71
|
+
);
|
|
72
|
+
return [
|
|
73
|
+
"## Possibly-relevant prior sessions (nlm-memory)",
|
|
74
|
+
...lines,
|
|
75
|
+
"NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved)."
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/core/hook/select.ts
|
|
80
|
+
function selectHits(params) {
|
|
81
|
+
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
|
|
82
|
+
const eligible = hits.filter(
|
|
83
|
+
(h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id)
|
|
84
|
+
);
|
|
85
|
+
const budget = Math.max(0, perConversationCap - surfaced.size);
|
|
86
|
+
const limit = Math.min(perFireCap, budget);
|
|
87
|
+
return eligible.slice(0, limit);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/hook/prompt-recall-hook.ts
|
|
91
|
+
var SCORE_THRESHOLD = 0;
|
|
92
|
+
var PER_FIRE_CAP = 3;
|
|
93
|
+
var PER_CONVERSATION_CAP = 10;
|
|
94
|
+
var RECALL_LIMIT = 5;
|
|
95
|
+
var RECALL_TIMEOUT_MS = 2e3;
|
|
96
|
+
var PROMPT_PREVIEW_CHARS = 200;
|
|
97
|
+
async function runHook(input, deps) {
|
|
98
|
+
const gate = classifyPrompt(input.prompt);
|
|
99
|
+
const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
|
|
100
|
+
if (gate === "generative") {
|
|
101
|
+
appendHookLog({
|
|
102
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
103
|
+
conversationId: input.conversationId,
|
|
104
|
+
promptPreview: preview,
|
|
105
|
+
gate,
|
|
106
|
+
hits: [],
|
|
107
|
+
wouldInject: [],
|
|
108
|
+
estTokens: 0,
|
|
109
|
+
mode: deps.mode
|
|
110
|
+
});
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
let hits = [];
|
|
114
|
+
try {
|
|
115
|
+
hits = await deps.recall(input.prompt);
|
|
116
|
+
} catch {
|
|
117
|
+
hits = [];
|
|
118
|
+
}
|
|
119
|
+
const surfaced = loadSurfaced(input.conversationId);
|
|
120
|
+
const selected = selectHits({
|
|
121
|
+
hits,
|
|
122
|
+
surfaced,
|
|
123
|
+
scoreThreshold: SCORE_THRESHOLD,
|
|
124
|
+
perFireCap: PER_FIRE_CAP,
|
|
125
|
+
perConversationCap: PER_CONVERSATION_CAP
|
|
126
|
+
});
|
|
127
|
+
const block = formatPointerBlock(selected);
|
|
128
|
+
const estTokens = Math.ceil(block.length / 4);
|
|
129
|
+
appendHookLog({
|
|
130
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
131
|
+
conversationId: input.conversationId,
|
|
132
|
+
promptPreview: preview,
|
|
133
|
+
gate,
|
|
134
|
+
hits: hits.map((h) => ({ id: h.id, score: h.matchScore })),
|
|
135
|
+
wouldInject: selected.map((h) => h.id),
|
|
136
|
+
estTokens,
|
|
137
|
+
mode: deps.mode
|
|
138
|
+
});
|
|
139
|
+
if (deps.mode === "live" && selected.length > 0) {
|
|
140
|
+
recordSurfaced(input.conversationId, selected.map((h) => h.id));
|
|
141
|
+
return block;
|
|
142
|
+
}
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
function readStdin() {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
let data = "";
|
|
148
|
+
process.stdin.setEncoding("utf8");
|
|
149
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
150
|
+
process.stdin.on("end", () => resolve(data));
|
|
151
|
+
process.stdin.on("error", () => resolve(data));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function recallOverHttp(prompt) {
|
|
155
|
+
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
156
|
+
const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}`;
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
|
|
159
|
+
try {
|
|
160
|
+
const res = await fetch(url, {
|
|
161
|
+
headers: { "x-recall-source": "hook" },
|
|
162
|
+
signal: controller.signal
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) return [];
|
|
165
|
+
let body;
|
|
166
|
+
try {
|
|
167
|
+
body = await res.json();
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
return (body.results ?? []).map((r) => ({
|
|
172
|
+
id: r.id,
|
|
173
|
+
label: r.label,
|
|
174
|
+
startedAt: r.startedAt,
|
|
175
|
+
matchScore: r.matchScore
|
|
176
|
+
}));
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function main() {
|
|
182
|
+
try {
|
|
183
|
+
const raw = await readStdin();
|
|
184
|
+
const payload = JSON.parse(raw);
|
|
185
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
186
|
+
const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
187
|
+
if (!prompt) return;
|
|
188
|
+
const mode = process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
|
|
189
|
+
const out = await runHook(
|
|
190
|
+
{ prompt, conversationId },
|
|
191
|
+
{ mode, recall: recallOverHttp }
|
|
192
|
+
);
|
|
193
|
+
if (out) process.stdout.write(out);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
198
|
+
void main();
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
runHook
|
|
202
|
+
};
|