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,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hook/stop-hook.ts
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
6
|
+
import { homedir as homedir3 } from "node:os";
|
|
7
|
+
import { dirname, join as join3 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/core/hook/citation-detect.ts
|
|
10
|
+
var MIN_ID_LEN = 6;
|
|
11
|
+
function detectCitations(input) {
|
|
12
|
+
const surfaced = [];
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const id of input.surfacedIds) {
|
|
15
|
+
if (id.length < MIN_ID_LEN) continue;
|
|
16
|
+
if (seen.has(id)) continue;
|
|
17
|
+
seen.add(id);
|
|
18
|
+
surfaced.push(id);
|
|
19
|
+
}
|
|
20
|
+
const cited = [];
|
|
21
|
+
const claimedByToolUse = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const tu of input.toolUses) {
|
|
23
|
+
if (!isNlmTool(tu.name)) continue;
|
|
24
|
+
if (isCiteSessionTool(tu.name)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const serialized = safeStringify(tu.input);
|
|
28
|
+
if (!serialized) continue;
|
|
29
|
+
for (const id of surfaced) {
|
|
30
|
+
if (claimedByToolUse.has(id)) continue;
|
|
31
|
+
if (serialized.includes(id)) {
|
|
32
|
+
cited.push({ id, kind: "tool_use" });
|
|
33
|
+
claimedByToolUse.add(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (input.responseText) {
|
|
38
|
+
for (const id of surfaced) {
|
|
39
|
+
if (claimedByToolUse.has(id)) continue;
|
|
40
|
+
if (input.responseText.includes(id)) {
|
|
41
|
+
cited.push({ id, kind: "prose" });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return cited;
|
|
46
|
+
}
|
|
47
|
+
function isNlmTool(name) {
|
|
48
|
+
return /^mcp__[^_]*nlm[^_]*__/.test(name);
|
|
49
|
+
}
|
|
50
|
+
function isCiteSessionTool(name) {
|
|
51
|
+
return name.endsWith("__cite_session");
|
|
52
|
+
}
|
|
53
|
+
function safeStringify(value) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(value);
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/core/hook/memo.ts
|
|
62
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
63
|
+
import { homedir } from "node:os";
|
|
64
|
+
import { join } from "node:path";
|
|
65
|
+
function stateDir() {
|
|
66
|
+
return process.env["NLM_HOOK_STATE_DIR"] ?? join(homedir(), ".nlm", "hook-state");
|
|
67
|
+
}
|
|
68
|
+
function memoPath(conversationId) {
|
|
69
|
+
const safe = conversationId.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
|
|
70
|
+
return join(stateDir(), `${safe}.json`);
|
|
71
|
+
}
|
|
72
|
+
function loadSurfaced(conversationId) {
|
|
73
|
+
try {
|
|
74
|
+
const path = memoPath(conversationId);
|
|
75
|
+
if (!existsSync(path)) return /* @__PURE__ */ new Set();
|
|
76
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
78
|
+
return new Set(parsed.filter((x) => typeof x === "string"));
|
|
79
|
+
} catch {
|
|
80
|
+
return /* @__PURE__ */ new Set();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/core/hook/cite-memo.ts
|
|
85
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
86
|
+
import { homedir as homedir2 } from "node:os";
|
|
87
|
+
import { join as join2 } from "node:path";
|
|
88
|
+
function stateDir2() {
|
|
89
|
+
return process.env["NLM_HOOK_STATE_DIR"] ?? join2(homedir2(), ".nlm", "hook-state");
|
|
90
|
+
}
|
|
91
|
+
function memoPath2(conversationId) {
|
|
92
|
+
const safe = conversationId.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
|
|
93
|
+
return join2(stateDir2(), `${safe}.cited.json`);
|
|
94
|
+
}
|
|
95
|
+
function loadCited(conversationId) {
|
|
96
|
+
try {
|
|
97
|
+
const path = memoPath2(conversationId);
|
|
98
|
+
if (!existsSync2(path)) return /* @__PURE__ */ new Set();
|
|
99
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
100
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
101
|
+
return new Set(parsed.filter((x) => typeof x === "string"));
|
|
102
|
+
} catch {
|
|
103
|
+
return /* @__PURE__ */ new Set();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function recordCited(conversationId, ids) {
|
|
107
|
+
if (ids.length === 0) return;
|
|
108
|
+
try {
|
|
109
|
+
const merged = loadCited(conversationId);
|
|
110
|
+
for (const id of ids) merged.add(id);
|
|
111
|
+
mkdirSync2(stateDir2(), { recursive: true });
|
|
112
|
+
writeFileSync2(memoPath2(conversationId), JSON.stringify([...merged]), "utf8");
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/core/hook/transcript.ts
|
|
118
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
119
|
+
function parseTurn(parsed) {
|
|
120
|
+
if (parsed.type !== "assistant" || !parsed.message) return null;
|
|
121
|
+
const content = parsed.message.content;
|
|
122
|
+
if (typeof content === "string") {
|
|
123
|
+
return content ? { text: content, toolUses: [] } : null;
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(content)) return null;
|
|
126
|
+
const textParts = [];
|
|
127
|
+
const toolUses = [];
|
|
128
|
+
for (const block of content) {
|
|
129
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
130
|
+
textParts.push(block.text);
|
|
131
|
+
} else if (block.type === "tool_use" && typeof block.name === "string") {
|
|
132
|
+
toolUses.push({ name: block.name, input: block.input });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (textParts.length === 0 && toolUses.length === 0) return null;
|
|
136
|
+
return { text: textParts.join("\n"), toolUses };
|
|
137
|
+
}
|
|
138
|
+
function readLines(transcriptPath) {
|
|
139
|
+
if (!transcriptPath || !existsSync3(transcriptPath)) return null;
|
|
140
|
+
try {
|
|
141
|
+
return readFileSync3(transcriptPath, "utf8").split("\n");
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function readAllAssistantTurns(transcriptPath) {
|
|
147
|
+
const lines = readLines(transcriptPath);
|
|
148
|
+
if (!lines) return [];
|
|
149
|
+
const turns = [];
|
|
150
|
+
for (const raw of lines) {
|
|
151
|
+
const line = raw.trim();
|
|
152
|
+
if (!line) continue;
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(line);
|
|
156
|
+
} catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const turn = parseTurn(parsed);
|
|
160
|
+
if (turn) turns.push(turn);
|
|
161
|
+
}
|
|
162
|
+
return turns;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/hook/stop-hook.ts
|
|
166
|
+
var RESPONSE_PREVIEW_CHARS = 200;
|
|
167
|
+
var POST_TIMEOUT_MS = 1500;
|
|
168
|
+
async function runStopHook(input, deps) {
|
|
169
|
+
if (input.stopHookActive) {
|
|
170
|
+
return {
|
|
171
|
+
conversationId: input.conversationId,
|
|
172
|
+
surfacedCount: 0,
|
|
173
|
+
citations: [],
|
|
174
|
+
responsePreview: "",
|
|
175
|
+
skipped: true
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const surfaced = loadSurfaced(input.conversationId);
|
|
179
|
+
if (surfaced.size === 0) {
|
|
180
|
+
return {
|
|
181
|
+
conversationId: input.conversationId,
|
|
182
|
+
surfacedCount: 0,
|
|
183
|
+
citations: [],
|
|
184
|
+
responsePreview: "",
|
|
185
|
+
skipped: false
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const turns = readAllAssistantTurns(input.transcriptPath);
|
|
189
|
+
if (turns.length === 0) {
|
|
190
|
+
return {
|
|
191
|
+
conversationId: input.conversationId,
|
|
192
|
+
surfacedCount: surfaced.size,
|
|
193
|
+
citations: [],
|
|
194
|
+
responsePreview: "",
|
|
195
|
+
skipped: false
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const allToolUses = [];
|
|
199
|
+
const textParts = [];
|
|
200
|
+
for (const turn of turns) {
|
|
201
|
+
if (turn.text) textParts.push(turn.text);
|
|
202
|
+
for (const tu of turn.toolUses) allToolUses.push(tu);
|
|
203
|
+
}
|
|
204
|
+
const unionText = textParts.join("\n");
|
|
205
|
+
const detected = detectCitations({
|
|
206
|
+
responseText: unionText,
|
|
207
|
+
toolUses: allToolUses,
|
|
208
|
+
surfacedIds: surfaced
|
|
209
|
+
});
|
|
210
|
+
const alreadyCited = loadCited(input.conversationId);
|
|
211
|
+
const fresh = detected.filter((c) => !alreadyCited.has(c.id));
|
|
212
|
+
const lastText = turns[turns.length - 1]?.text ?? "";
|
|
213
|
+
const preview = lastText.slice(0, RESPONSE_PREVIEW_CHARS);
|
|
214
|
+
for (const c of fresh) {
|
|
215
|
+
try {
|
|
216
|
+
await deps.postCitation(input.conversationId, c.id, c.kind, preview);
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (fresh.length > 0) {
|
|
221
|
+
recordCited(input.conversationId, fresh.map((c) => c.id));
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
conversationId: input.conversationId,
|
|
225
|
+
surfacedCount: surfaced.size,
|
|
226
|
+
citations: fresh,
|
|
227
|
+
responsePreview: preview,
|
|
228
|
+
skipped: false
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function logPath() {
|
|
232
|
+
return process.env["NLM_HOOK_LOG"] ?? join3(homedir3(), ".nlm", "hook-log.jsonl");
|
|
233
|
+
}
|
|
234
|
+
function logStopResult(result) {
|
|
235
|
+
try {
|
|
236
|
+
const path = logPath();
|
|
237
|
+
mkdirSync3(dirname(path), { recursive: true });
|
|
238
|
+
appendFileSync(
|
|
239
|
+
path,
|
|
240
|
+
`${JSON.stringify({
|
|
241
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
242
|
+
kind: "stop",
|
|
243
|
+
conversationId: result.conversationId,
|
|
244
|
+
surfacedCount: result.surfacedCount,
|
|
245
|
+
citedIds: result.citations.map((c) => c.id),
|
|
246
|
+
citationKinds: result.citations.map((c) => c.kind),
|
|
247
|
+
skipped: result.skipped,
|
|
248
|
+
mode: process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow"
|
|
249
|
+
})}
|
|
250
|
+
`,
|
|
251
|
+
"utf8"
|
|
252
|
+
);
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function readStdin() {
|
|
257
|
+
return new Promise((resolve) => {
|
|
258
|
+
let data = "";
|
|
259
|
+
process.stdin.setEncoding("utf8");
|
|
260
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
261
|
+
process.stdin.on("end", () => resolve(data));
|
|
262
|
+
process.stdin.on("error", () => resolve(data));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function postCitationOverHttp(conversationId, citedId, kind, responsePreview) {
|
|
266
|
+
const port = process.env["NLM_PORT"] ?? "3940";
|
|
267
|
+
const url = `http://localhost:${port}/api/recall/cite-event`;
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const timer = setTimeout(() => controller.abort(), POST_TIMEOUT_MS);
|
|
270
|
+
try {
|
|
271
|
+
await fetch(url, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: { "content-type": "application/json" },
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
conversation_id: conversationId,
|
|
276
|
+
cited_id: citedId,
|
|
277
|
+
kind,
|
|
278
|
+
response_preview: responsePreview
|
|
279
|
+
}),
|
|
280
|
+
signal: controller.signal
|
|
281
|
+
});
|
|
282
|
+
} finally {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function main() {
|
|
287
|
+
try {
|
|
288
|
+
const raw = await readStdin();
|
|
289
|
+
const payload = JSON.parse(raw);
|
|
290
|
+
const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
|
291
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
292
|
+
const stopHookActive = payload.stop_hook_active === true;
|
|
293
|
+
const result = await runStopHook(
|
|
294
|
+
{ conversationId, transcriptPath, stopHookActive },
|
|
295
|
+
{ postCitation: postCitationOverHttp }
|
|
296
|
+
);
|
|
297
|
+
logStopResult(result);
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
302
|
+
void main();
|
|
303
|
+
}
|
|
304
|
+
export {
|
|
305
|
+
runStopHook
|
|
306
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# nlm-memory Hermes Agent plugin
|
|
2
|
+
|
|
3
|
+
This directory is the [NousResearch Hermes Agent](https://github.com/NousResearch/hermes-agent) plugin distribution surface for nlm-memory.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Prerequisite: `npm install -g nlm-memory` (puts `nlm` on PATH; the MCP server spawns `nlm mcp`).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Recommended — uses nlm connect to copy the plugin and enable it
|
|
11
|
+
nlm connect hermes-agent
|
|
12
|
+
|
|
13
|
+
# Or manually
|
|
14
|
+
cp -r plugin-hermes-agent ~/.hermes/plugins/nlm-memory
|
|
15
|
+
hermes plugins enable nlm-memory
|
|
16
|
+
nlm connect hermes # also writes the MCP server entry to ~/.hermes/config.yaml
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What this ships
|
|
20
|
+
|
|
21
|
+
- **`plugin.yaml`** — Hermes plugin manifest (name: `nlm-memory`, kind: `memory`)
|
|
22
|
+
- **`__init__.py`** — Python shim that registers 6 lifecycle hooks; all delegate to the local nlm daemon at `http://localhost:3940`
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
| Hermes hook | Action |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `pre_llm_call` | Keyword-recalls up to 3 relevant prior sessions; injects them as context above the user message |
|
|
29
|
+
| `post_llm_call` | Scans the assistant response for cited session IDs and logs prose citation events |
|
|
30
|
+
| `on_session_start` | No-op (memo is created lazily on first recall hit) |
|
|
31
|
+
| `on_session_end` | Clears the per-session surfaced-ID memo |
|
|
32
|
+
| `on_session_finalize` | Clears the per-session surfaced-ID memo |
|
|
33
|
+
| `on_session_reset` | Clears the per-session surfaced-ID memo |
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
| Env var | Default | Purpose |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `NLM_DAEMON_PORT` | `3940` | Port the nlm daemon listens on |
|
|
40
|
+
|
|
41
|
+
## Daemon required
|
|
42
|
+
|
|
43
|
+
The plugin delegates entirely to the nlm daemon. Start it with `nlm start` or `nlm install` (macOS LaunchAgent). If the daemon is unreachable, every hook silently returns `None` — the agent loop is never blocked.
|
|
44
|
+
|
|
45
|
+
## Uninstall
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
nlm disconnect hermes-agent
|
|
49
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nlm-memory Hermes Agent plugin.
|
|
3
|
+
|
|
4
|
+
Wires 6 Hermes lifecycle hooks to the local nlm-memory daemon
|
|
5
|
+
(http://localhost:3940 by default). All calls are fire-and-forget
|
|
6
|
+
except pre_llm_call, which returns a context string for injection.
|
|
7
|
+
|
|
8
|
+
The daemon must be running for hooks to have effect. If it is unreachable,
|
|
9
|
+
every hook silently returns None so the agent loop is never blocked.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.request
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _base_url() -> str:
|
|
20
|
+
port = os.environ.get("NLM_DAEMON_PORT", "3940")
|
|
21
|
+
return f"http://localhost:{port}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _post(path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
url = f"{_base_url()}/api/{path}"
|
|
26
|
+
data = json.dumps(payload).encode()
|
|
27
|
+
req = urllib.request.Request(
|
|
28
|
+
url,
|
|
29
|
+
data=data,
|
|
30
|
+
headers={"Content-Type": "application/json"},
|
|
31
|
+
method="POST",
|
|
32
|
+
)
|
|
33
|
+
try:
|
|
34
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
35
|
+
return json.loads(resp.read())
|
|
36
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError):
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register(ctx: Any) -> None:
|
|
41
|
+
def pre_llm_call(
|
|
42
|
+
session_id: str,
|
|
43
|
+
user_message: str,
|
|
44
|
+
is_first_turn: bool = False,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> str | None:
|
|
47
|
+
result = _post(
|
|
48
|
+
"hook/hermes-agent/pre-turn",
|
|
49
|
+
{"session_id": session_id, "user_message": user_message, "is_first_turn": is_first_turn},
|
|
50
|
+
)
|
|
51
|
+
context = result.get("context")
|
|
52
|
+
return context if isinstance(context, str) and context else None
|
|
53
|
+
|
|
54
|
+
def post_llm_call(
|
|
55
|
+
session_id: str,
|
|
56
|
+
assistant_response: str,
|
|
57
|
+
**kwargs: Any,
|
|
58
|
+
) -> None:
|
|
59
|
+
_post(
|
|
60
|
+
"hook/hermes-agent/post-turn",
|
|
61
|
+
{"session_id": session_id, "assistant_response": assistant_response},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _session_event(event: str, session_id: str | None, **kwargs: Any) -> None:
|
|
65
|
+
_post(
|
|
66
|
+
"hook/hermes-agent/session-lifecycle",
|
|
67
|
+
{"event": event, "session_id": session_id},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
ctx.register_hook("pre_llm_call", pre_llm_call)
|
|
71
|
+
ctx.register_hook("post_llm_call", post_llm_call)
|
|
72
|
+
ctx.register_hook("on_session_start", lambda session_id, **kw: _session_event("start", session_id, **kw))
|
|
73
|
+
ctx.register_hook("on_session_end", lambda session_id, **kw: _session_event("end", session_id, **kw))
|
|
74
|
+
ctx.register_hook("on_session_finalize", lambda session_id=None, **kw: _session_event("finalize", session_id, **kw))
|
|
75
|
+
ctx.register_hook("on_session_reset", lambda session_id, **kw: _session_event("reset", session_id, **kw))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: nlm-memory
|
|
2
|
+
version: 0.1.0
|
|
3
|
+
description: Non-linear memory for Hermes — surfaces relevant prior sessions at each turn via keyword recall
|
|
4
|
+
kind: memory
|
|
5
|
+
author: pbmagnet4
|
|
6
|
+
provides_hooks:
|
|
7
|
+
- pre_llm_call
|
|
8
|
+
- post_llm_call
|
|
9
|
+
- on_session_start
|
|
10
|
+
- on_session_end
|
|
11
|
+
- on_session_finalize
|
|
12
|
+
- on_session_reset
|
|
13
|
+
requires_env:
|
|
14
|
+
- name: NLM_DAEMON_PORT
|
|
15
|
+
description: "Port the nlm-memory daemon listens on (default: 3940)"
|
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bundles each hook entry under src/hook/ as a single-file .mjs into
|
|
4
|
+
* plugin/scripts/, ready for Codex to invoke via ${CLAUDE_PLUGIN_ROOT}.
|
|
5
|
+
*
|
|
6
|
+
* Why single-file bundles: Codex plugins are content-addressed by file hash
|
|
7
|
+
* for the trust system. Shipping a tree of imports would force the user to
|
|
8
|
+
* re-trust every release across dozens of files. One file per hook keeps
|
|
9
|
+
* the trust footprint minimal.
|
|
10
|
+
*
|
|
11
|
+
* Why esbuild not tsc: tsc emits a tree of files with relative imports; the
|
|
12
|
+
* plugin would have to ship `dist/` plus its dependency tree. esbuild flattens
|
|
13
|
+
* to one .mjs per entry with all internal deps inlined. node builtins are
|
|
14
|
+
* left as-is (node:*) and zero runtime deps are bundled because the hook
|
|
15
|
+
* scripts only talk to the daemon over HTTP — no native bindings required.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { build } from "esbuild";
|
|
19
|
+
import { resolve, dirname } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { mkdirSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const REPO_ROOT = resolve(__dirname, "..");
|
|
25
|
+
const OUT_DIR = resolve(REPO_ROOT, "plugin/scripts");
|
|
26
|
+
|
|
27
|
+
const ENTRIES = [
|
|
28
|
+
"src/hook/prompt-recall-hook.ts",
|
|
29
|
+
"src/hook/stop-hook.ts",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
mkdirSync(OUT_DIR, { recursive: true });
|
|
33
|
+
|
|
34
|
+
const results = await Promise.all(
|
|
35
|
+
ENTRIES.map(async (entry) => {
|
|
36
|
+
const entryPath = resolve(REPO_ROOT, entry);
|
|
37
|
+
const outName = entry.split("/").pop().replace(/\.ts$/, ".mjs");
|
|
38
|
+
const outFile = resolve(OUT_DIR, outName);
|
|
39
|
+
const result = await build({
|
|
40
|
+
entryPoints: [entryPath],
|
|
41
|
+
outfile: outFile,
|
|
42
|
+
bundle: true,
|
|
43
|
+
platform: "node",
|
|
44
|
+
format: "esm",
|
|
45
|
+
target: "node20",
|
|
46
|
+
minify: false,
|
|
47
|
+
sourcemap: false,
|
|
48
|
+
legalComments: "none",
|
|
49
|
+
logLevel: "warning",
|
|
50
|
+
banner: { js: "#!/usr/bin/env node" },
|
|
51
|
+
tsconfig: resolve(REPO_ROOT, "tsconfig.json"),
|
|
52
|
+
});
|
|
53
|
+
return { entry, outFile, warnings: result.warnings.length };
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
for (const r of results) {
|
|
58
|
+
process.stdout.write(`bundled ${r.entry} -> ${r.outFile.replace(REPO_ROOT + "/", "")}`);
|
|
59
|
+
if (r.warnings > 0) process.stdout.write(` (${r.warnings} warnings)`);
|
|
60
|
+
process.stdout.write("\n");
|
|
61
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Direct DeepSeek probe — calls the API with the same prompt + a known-failing
|
|
3
|
+
// session body and dumps everything: raw choice content, finish_reason, usage.
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
|
|
8
|
+
const envContent = readFileSync(`${process.env.HOME}/.nlm/.env`, "utf8");
|
|
9
|
+
const apiKey = envContent.split("\n").find((l) => l.startsWith("DEEPSEEK_API_KEY="))?.split("=")[1]?.trim();
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
console.error("no DEEPSEEK_API_KEY in ~/.nlm/.env");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const db = new Database(`${process.env.HOME}/.nlm/canonical.sqlite`, { readonly: true });
|
|
16
|
+
const targets = process.argv.slice(2);
|
|
17
|
+
if (targets.length === 0) {
|
|
18
|
+
console.error("usage: deepseek-probe.mjs <session-id> [more ids...]");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Pull the actual classifier prompt from the build
|
|
23
|
+
const promptModule = await import("/Users/echalupa/Documents/Coding Projects/nlm-memory-ts/src/core/classifier/prompt.ts");
|
|
24
|
+
|
|
25
|
+
for (const sid of targets) {
|
|
26
|
+
const row = db.prepare("SELECT body FROM sessions WHERE id = ?").get(sid);
|
|
27
|
+
if (!row?.body) {
|
|
28
|
+
console.log(`\n=== ${sid}: no body ===`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const userPrompt = promptModule.buildUserPrompt(row.body, "");
|
|
32
|
+
console.log(`\n=== ${sid} (body ${row.body.length} chars, prompt ${userPrompt.length} chars) ===`);
|
|
33
|
+
|
|
34
|
+
const res = await fetch("https://api.deepseek.com/v1/chat/completions", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
model: "deepseek-v4-flash",
|
|
39
|
+
messages: [
|
|
40
|
+
{ role: "system", content: promptModule.CLASSIFIER_SYSTEM_PROMPT },
|
|
41
|
+
{ role: "user", content: userPrompt },
|
|
42
|
+
],
|
|
43
|
+
response_format: { type: "json_object" },
|
|
44
|
+
temperature: 0.1,
|
|
45
|
+
max_tokens: 8192,
|
|
46
|
+
stream: false,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
console.log(`HTTP ${res.status}: ${(await res.text()).slice(0, 500)}`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const data = await res.json();
|
|
54
|
+
const choice = data.choices?.[0];
|
|
55
|
+
console.log(`finish_reason: ${choice?.finish_reason}`);
|
|
56
|
+
console.log(`usage: ${JSON.stringify(data.usage)}`);
|
|
57
|
+
const content = choice?.message?.content ?? "";
|
|
58
|
+
console.log(`content (${content.length} chars):`);
|
|
59
|
+
console.log(content);
|
|
60
|
+
console.log("--- end content ---");
|
|
61
|
+
try {
|
|
62
|
+
JSON.parse(content.replace(/^```(?:json)?\s*|\s*```$/gm, "").trim());
|
|
63
|
+
console.log("JSON.parse: OK");
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.log(`JSON.parse: FAILED — ${e.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|