nlm-memory 0.20.0 → 0.20.1
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/README.md +17 -10
- package/dist/cli/digest.js +21 -0
- package/dist/cli/digest.js.map +1 -1
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +49 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/nlm.d.ts +13 -1
- package/dist/cli/nlm.js +766 -55
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/actions/overlay.d.ts +20 -6
- package/dist/core/actions/overlay.js +32 -18
- package/dist/core/actions/overlay.js.map +1 -1
- package/dist/core/adapters/claude-code.js +6 -0
- package/dist/core/adapters/claude-code.js.map +1 -1
- package/dist/core/classifier/prompt.js +3 -3
- package/dist/core/digest/compose.d.ts +10 -0
- package/dist/core/digest/compose.js +7 -1
- package/dist/core/digest/compose.js.map +1 -1
- package/dist/core/embedding/embed-backfill.d.ts +13 -2
- package/dist/core/embedding/embed-backfill.js +181 -43
- package/dist/core/embedding/embed-backfill.js.map +1 -1
- package/dist/core/embedding/embedding-config.d.ts +17 -0
- package/dist/core/embedding/embedding-config.js +12 -0
- package/dist/core/embedding/embedding-config.js.map +1 -0
- package/dist/core/embedding/pg-embed-backfill.d.ts +31 -0
- package/dist/core/embedding/pg-embed-backfill.js +264 -0
- package/dist/core/embedding/pg-embed-backfill.js.map +1 -0
- package/dist/core/entities/dedup-suggest.d.ts +25 -0
- package/dist/core/entities/dedup-suggest.js +107 -0
- package/dist/core/entities/dedup-suggest.js.map +1 -0
- package/dist/core/eval/classifier-fixture-eval.d.ts +29 -0
- package/dist/core/eval/classifier-fixture-eval.js +217 -0
- package/dist/core/eval/classifier-fixture-eval.js.map +1 -0
- package/dist/core/eval/extraction-scoring.d.ts +63 -0
- package/dist/core/eval/extraction-scoring.js +79 -0
- package/dist/core/eval/extraction-scoring.js.map +1 -0
- package/dist/core/eval/run-eval.d.ts +40 -0
- package/dist/core/eval/run-eval.js +34 -0
- package/dist/core/eval/run-eval.js.map +1 -0
- package/dist/core/exemplars/capture-from-session.d.ts +1 -0
- package/dist/core/exemplars/capture-from-session.js +6 -2
- package/dist/core/exemplars/capture-from-session.js.map +1 -1
- package/dist/core/exemplars/diff-parse.d.ts +23 -0
- package/dist/core/exemplars/diff-parse.js +73 -0
- package/dist/core/exemplars/diff-parse.js.map +1 -0
- package/dist/core/exemplars/embed-backfill.d.ts +36 -0
- package/dist/core/exemplars/embed-backfill.js +85 -0
- package/dist/core/exemplars/embed-backfill.js.map +1 -0
- package/dist/core/exemplars/embed-text.d.ts +15 -0
- package/dist/core/exemplars/embed-text.js +18 -0
- package/dist/core/exemplars/embed-text.js.map +1 -0
- package/dist/core/exemplars/extract-exemplar.js +2 -65
- package/dist/core/exemplars/extract-exemplar.js.map +1 -1
- package/dist/core/exemplars/recall-code.js +4 -0
- package/dist/core/exemplars/recall-code.js.map +1 -1
- package/dist/core/exemplars/warm-embedder.d.ts +14 -0
- package/dist/core/exemplars/warm-embedder.js +18 -0
- package/dist/core/exemplars/warm-embedder.js.map +1 -0
- package/dist/core/health/corpus-state.d.ts +8 -0
- package/dist/core/health/corpus-state.js +11 -0
- package/dist/core/health/corpus-state.js.map +1 -0
- package/dist/core/health/embed-inflight.d.ts +9 -0
- package/dist/core/health/embed-inflight.js +30 -0
- package/dist/core/health/embed-inflight.js.map +1 -0
- package/dist/core/health/embedding-lane-state.d.ts +6 -0
- package/dist/core/health/embedding-lane-state.js +18 -0
- package/dist/core/health/embedding-lane-state.js.map +1 -0
- package/dist/core/health/warmup-state.d.ts +12 -0
- package/dist/core/health/warmup-state.js +12 -0
- package/dist/core/health/warmup-state.js.map +1 -0
- package/dist/core/hook/gate.d.ts +13 -6
- package/dist/core/hook/gate.js +47 -6
- package/dist/core/hook/gate.js.map +1 -1
- package/dist/core/hook/hook-log.d.ts +5 -0
- package/dist/core/hook/hook-log.js.map +1 -1
- package/dist/core/hook/memo.d.ts +12 -0
- package/dist/core/hook/memo.js +41 -1
- package/dist/core/hook/memo.js.map +1 -1
- package/dist/core/hook/pointer-block.d.ts +1 -0
- package/dist/core/hook/pointer-block.js +23 -2
- package/dist/core/hook/pointer-block.js.map +1 -1
- package/dist/core/hook/query-extract.d.ts +2 -1
- package/dist/core/hook/query-extract.js +10 -1
- package/dist/core/hook/query-extract.js.map +1 -1
- package/dist/core/hook/resolve-conversation-by-query.d.ts +28 -0
- package/dist/core/hook/resolve-conversation-by-query.js +98 -0
- package/dist/core/hook/resolve-conversation-by-query.js.map +1 -0
- package/dist/core/hook/select.d.ts +8 -0
- package/dist/core/hook/select.js +9 -2
- package/dist/core/hook/select.js.map +1 -1
- package/dist/core/hook/transcript.js +16 -2
- package/dist/core/hook/transcript.js.map +1 -1
- package/dist/core/ingest/ingest-session.d.ts +5 -0
- package/dist/core/ingest/ingest-session.js +3 -0
- package/dist/core/ingest/ingest-session.js.map +1 -1
- package/dist/core/ingest/reclassify-oversized.d.ts +5 -0
- package/dist/core/ingest/reclassify-oversized.js +3 -0
- package/dist/core/ingest/reclassify-oversized.js.map +1 -1
- package/dist/core/ingest/reprocess.d.ts +96 -0
- package/dist/core/ingest/reprocess.js +275 -0
- package/dist/core/ingest/reprocess.js.map +1 -0
- package/dist/core/integrity/check-invariants.d.ts +6 -1
- package/dist/core/integrity/check-invariants.js +209 -3
- package/dist/core/integrity/check-invariants.js.map +1 -1
- package/dist/core/metrics/corpus-stats.d.ts +49 -0
- package/dist/core/metrics/corpus-stats.js +111 -0
- package/dist/core/metrics/corpus-stats.js.map +1 -0
- package/dist/core/metrics/re-derivation.d.ts +48 -0
- package/dist/core/metrics/re-derivation.js +87 -0
- package/dist/core/metrics/re-derivation.js.map +1 -0
- package/dist/core/recall/citation-log.d.ts +8 -0
- package/dist/core/recall/citation-log.js +17 -0
- package/dist/core/recall/citation-log.js.map +1 -1
- package/dist/core/recall/query-intent.d.ts +7 -0
- package/dist/core/recall/query-intent.js +25 -0
- package/dist/core/recall/query-intent.js.map +1 -0
- package/dist/core/recall/query-log.d.ts +4 -0
- package/dist/core/recall/query-log.js +4 -0
- package/dist/core/recall/query-log.js.map +1 -1
- package/dist/core/recall/recall-service.d.ts +37 -1
- package/dist/core/recall/recall-service.js +106 -63
- package/dist/core/recall/recall-service.js.map +1 -1
- package/dist/core/recall/related-exemplars.d.ts +1 -0
- package/dist/core/recall/related-exemplars.js +4 -1
- package/dist/core/recall/related-exemplars.js.map +1 -1
- package/dist/core/recall/related-facts.js +2 -2
- package/dist/core/recall/related-facts.js.map +1 -1
- package/dist/core/recall/reranker.d.ts +2 -1
- package/dist/core/recall/reranker.js +3 -3
- package/dist/core/recall/reranker.js.map +1 -1
- package/dist/core/recall-facts/fact-query-log.d.ts +7 -0
- package/dist/core/recall-facts/fact-query-log.js +2 -0
- package/dist/core/recall-facts/fact-query-log.js.map +1 -1
- package/dist/core/recall-facts/fact-recall-service.d.ts +9 -1
- package/dist/core/recall-facts/fact-recall-service.js +21 -8
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scan-once.d.ts +6 -6
- package/dist/core/scheduler/scan-once.js +13 -49
- package/dist/core/scheduler/scan-once.js.map +1 -1
- package/dist/core/scheduler/scheduler.d.ts +10 -0
- package/dist/core/scheduler/scheduler.js +75 -10
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/signals/code-signal.d.ts +49 -0
- package/dist/core/signals/code-signal.js +53 -0
- package/dist/core/signals/code-signal.js.map +1 -0
- package/dist/core/storage/db-restore.js +3 -1
- package/dist/core/storage/db-restore.js.map +1 -1
- package/dist/core/storage/fact-batch.d.ts +9 -0
- package/dist/core/storage/fact-batch.js +14 -0
- package/dist/core/storage/fact-batch.js.map +1 -0
- package/dist/core/storage/pg-code-exemplar-store.d.ts +1 -0
- package/dist/core/storage/pg-code-exemplar-store.js +7 -0
- package/dist/core/storage/pg-code-exemplar-store.js.map +1 -1
- package/dist/core/storage/pg-embedding-config.d.ts +29 -0
- package/dist/core/storage/pg-embedding-config.js +53 -0
- package/dist/core/storage/pg-embedding-config.js.map +1 -0
- package/dist/core/storage/pg-entity-store.d.ts +12 -0
- package/dist/core/storage/pg-entity-store.js +79 -0
- package/dist/core/storage/pg-entity-store.js.map +1 -0
- package/dist/core/storage/pg-fact-ingest.d.ts +15 -0
- package/dist/core/storage/pg-fact-ingest.js +34 -0
- package/dist/core/storage/pg-fact-ingest.js.map +1 -0
- package/dist/core/storage/pg-fact-store.d.ts +3 -0
- package/dist/core/storage/pg-fact-store.js +26 -30
- package/dist/core/storage/pg-fact-store.js.map +1 -1
- package/dist/core/storage/pg-migrate.d.ts +18 -0
- package/dist/core/storage/pg-migrate.js +73 -0
- package/dist/core/storage/pg-migrate.js.map +1 -0
- package/dist/core/storage/pg-session-store.d.ts +9 -0
- package/dist/core/storage/pg-session-store.js +204 -76
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/pg-storage.d.ts +9 -7
- package/dist/core/storage/pg-storage.js +20 -50
- package/dist/core/storage/pg-storage.js.map +1 -1
- package/dist/core/storage/pg-workstream-store.d.ts +24 -0
- package/dist/core/storage/pg-workstream-store.js +96 -0
- package/dist/core/storage/pg-workstream-store.js.map +1 -0
- package/dist/core/storage/sqlite-code-exemplar-store.d.ts +1 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js +11 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -1
- package/dist/core/storage/sqlite-embedding-config.d.ts +10 -0
- package/dist/core/storage/sqlite-embedding-config.js +27 -0
- package/dist/core/storage/sqlite-embedding-config.js.map +1 -0
- package/dist/core/storage/sqlite-entity-store.d.ts +13 -0
- package/dist/core/storage/sqlite-entity-store.js +96 -0
- package/dist/core/storage/sqlite-entity-store.js.map +1 -0
- package/dist/core/storage/sqlite-fact-store.d.ts +15 -7
- package/dist/core/storage/sqlite-fact-store.js +76 -25
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +20 -5
- package/dist/core/storage/sqlite-session-store.js +183 -68
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +8 -8
- package/dist/core/storage/sqlite-storage.js +21 -32
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/sqlite-workstream-store.d.ts +24 -0
- package/dist/core/storage/sqlite-workstream-store.js +98 -0
- package/dist/core/storage/sqlite-workstream-store.js.map +1 -0
- package/dist/core/work-digest/active-spans.d.ts +8 -0
- package/dist/core/work-digest/active-spans.js +29 -0
- package/dist/core/work-digest/active-spans.js.map +1 -0
- package/dist/core/work-digest/attribute.d.ts +15 -0
- package/dist/core/work-digest/attribute.js +76 -0
- package/dist/core/work-digest/attribute.js.map +1 -0
- package/dist/core/work-digest/build-work-digest.d.ts +12 -0
- package/dist/core/work-digest/build-work-digest.js +69 -0
- package/dist/core/work-digest/build-work-digest.js.map +1 -0
- package/dist/core/work-digest/compose-work-digest.d.ts +3 -0
- package/dist/core/work-digest/compose-work-digest.js +52 -0
- package/dist/core/work-digest/compose-work-digest.js.map +1 -0
- package/dist/core/work-digest/merge-active.d.ts +10 -0
- package/dist/core/work-digest/merge-active.js +22 -0
- package/dist/core/work-digest/merge-active.js.map +1 -0
- package/dist/core/work-digest/read-transcript-timestamps.d.ts +7 -0
- package/dist/core/work-digest/read-transcript-timestamps.js +40 -0
- package/dist/core/work-digest/read-transcript-timestamps.js.map +1 -0
- package/dist/core/work-digest/topics.d.ts +18 -0
- package/dist/core/work-digest/topics.js +26 -0
- package/dist/core/work-digest/topics.js.map +1 -0
- package/dist/core/work-digest/types.d.ts +45 -0
- package/dist/core/work-digest/types.js +2 -0
- package/dist/core/work-digest/types.js.map +1 -0
- package/dist/core/workstream/bind.d.ts +25 -0
- package/dist/core/workstream/bind.js +23 -0
- package/dist/core/workstream/bind.js.map +1 -0
- package/dist/core/workstream/compose-recall.d.ts +2 -0
- package/dist/core/workstream/compose-recall.js +39 -0
- package/dist/core/workstream/compose-recall.js.map +1 -0
- package/dist/core/workstream/merge-suggest.d.ts +19 -0
- package/dist/core/workstream/merge-suggest.js +61 -0
- package/dist/core/workstream/merge-suggest.js.map +1 -0
- package/dist/core/workstream/model.d.ts +20 -0
- package/dist/core/workstream/model.js +9 -0
- package/dist/core/workstream/model.js.map +1 -0
- package/dist/core/workstream/name-match.d.ts +8 -0
- package/dist/core/workstream/name-match.js +18 -0
- package/dist/core/workstream/name-match.js.map +1 -0
- package/dist/core/workstream/resolve.d.ts +10 -0
- package/dist/core/workstream/resolve.js +21 -0
- package/dist/core/workstream/resolve.js.map +1 -0
- package/dist/core/workstream/rollup.d.ts +12 -0
- package/dist/core/workstream/rollup.js +17 -0
- package/dist/core/workstream/rollup.js.map +1 -0
- package/dist/core/workstream/work-topics.d.ts +10 -0
- package/dist/core/workstream/work-topics.js +55 -0
- package/dist/core/workstream/work-topics.js.map +1 -0
- package/dist/hook/hook-helpers.d.ts +4 -0
- package/dist/hook/hook-helpers.js +36 -0
- package/dist/hook/hook-helpers.js.map +1 -0
- package/dist/hook/pre-compact-hook.d.ts +23 -0
- package/dist/hook/pre-compact-hook.js +66 -0
- package/dist/hook/pre-compact-hook.js.map +1 -0
- package/dist/hook/prompt-recall-hook.d.ts +37 -0
- package/dist/hook/prompt-recall-hook.js +120 -20
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/recall-gate.d.ts +29 -0
- package/dist/hook/recall-gate.js +50 -0
- package/dist/hook/recall-gate.js.map +1 -0
- package/dist/hook/recall-over-http.d.ts +1 -1
- package/dist/hook/recall-over-http.js +11 -11
- package/dist/hook/recall-over-http.js.map +1 -1
- package/dist/hook/recent-context.d.ts +25 -0
- package/dist/hook/recent-context.js +104 -0
- package/dist/hook/recent-context.js.map +1 -0
- package/dist/hook/score-floor.d.ts +27 -0
- package/dist/hook/score-floor.js +42 -0
- package/dist/hook/score-floor.js.map +1 -0
- package/dist/hook/session-end-hook.js +8 -29
- package/dist/hook/session-end-hook.js.map +1 -1
- package/dist/hook/session-start-hook.js +24 -59
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +22 -59
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/hook/subagent-start-hook.d.ts +28 -0
- package/dist/hook/subagent-start-hook.js +81 -0
- package/dist/hook/subagent-start-hook.js.map +1 -0
- package/dist/http/app.js +28 -8
- package/dist/http/app.js.map +1 -1
- package/dist/install/setup.js +2 -0
- package/dist/install/setup.js.map +1 -1
- package/dist/llm/build-classifier.d.ts +22 -0
- package/dist/llm/build-classifier.js +42 -0
- package/dist/llm/build-classifier.js.map +1 -0
- package/dist/llm/build-embedder.d.ts +11 -0
- package/dist/llm/build-embedder.js +42 -0
- package/dist/llm/build-embedder.js.map +1 -0
- package/dist/llm/bundled-embedder-client.d.ts +37 -0
- package/dist/llm/bundled-embedder-client.js +90 -0
- package/dist/llm/bundled-embedder-client.js.map +1 -0
- package/dist/llm/classifier-box.d.ts +13 -5
- package/dist/llm/classifier-box.js +28 -6
- package/dist/llm/classifier-box.js.map +1 -1
- package/dist/llm/classifier-egress.d.ts +18 -0
- package/dist/llm/classifier-egress.js +59 -0
- package/dist/llm/classifier-egress.js.map +1 -0
- package/dist/llm/client-shared.d.ts +4 -0
- package/dist/llm/client-shared.js +39 -0
- package/dist/llm/client-shared.js.map +1 -0
- package/dist/llm/deepseek-client.d.ts +20 -0
- package/dist/llm/deepseek-client.js +59 -45
- package/dist/llm/deepseek-client.js.map +1 -1
- package/dist/llm/embedder-info.d.ts +15 -0
- package/dist/llm/embedder-info.js +27 -0
- package/dist/llm/embedder-info.js.map +1 -0
- package/dist/llm/model-quirks.d.ts +4 -0
- package/dist/llm/model-quirks.js +7 -0
- package/dist/llm/model-quirks.js.map +1 -0
- package/dist/llm/naming.d.ts +5 -0
- package/dist/llm/naming.js +20 -0
- package/dist/llm/naming.js.map +1 -0
- package/dist/llm/ollama-client.d.ts +7 -8
- package/dist/llm/ollama-client.js +59 -51
- package/dist/llm/ollama-client.js.map +1 -1
- package/dist/llm/ollama-code-embedder.d.ts +8 -3
- package/dist/llm/ollama-code-embedder.js +14 -24
- package/dist/llm/ollama-code-embedder.js.map +1 -1
- package/dist/llm/openai-code-embedder-client.d.ts +25 -0
- package/dist/llm/openai-code-embedder-client.js +52 -0
- package/dist/llm/openai-code-embedder-client.js.map +1 -0
- package/dist/llm/openai-embedder-client.d.ts +41 -0
- package/dist/llm/openai-embedder-client.js +83 -0
- package/dist/llm/openai-embedder-client.js.map +1 -0
- package/dist/mcp/server.d.ts +47 -2
- package/dist/mcp/server.js +303 -18
- package/dist/mcp/server.js.map +1 -1
- package/dist/ports/code-embedder.d.ts +6 -1
- package/dist/ports/code-exemplar-store.d.ts +5 -0
- package/dist/ports/entity-store.d.ts +35 -0
- package/dist/ports/entity-store.js +7 -0
- package/dist/ports/entity-store.js.map +1 -0
- package/dist/ports/fact-store.d.ts +0 -0
- package/dist/ports/fact-store.js.map +1 -1
- package/dist/ports/llm-client.d.ts +9 -1
- package/dist/ports/session-store.d.ts +12 -0
- package/dist/ports/storage.d.ts +7 -30
- package/dist/ports/storage.js +3 -7
- package/dist/ports/storage.js.map +1 -1
- package/dist/ports/transcript-adapter.d.ts +2 -0
- package/dist/ports/workstream-store.d.ts +21 -0
- package/dist/ports/workstream-store.js +2 -0
- package/dist/ports/workstream-store.js.map +1 -0
- package/dist/shared/types.d.ts +23 -2
- package/fixtures/classifier-gold/README.md +94 -0
- package/fixtures/classifier-gold/reference.json +318 -0
- package/fixtures/classifier-gold/transcripts/01-bug-fix-null-pointer-api.txt +75 -0
- package/fixtures/classifier-gold/transcripts/02-bug-fix-race-condition-queue.txt +77 -0
- package/fixtures/classifier-gold/transcripts/03-bug-fix-jwt-clock-skew.txt +86 -0
- package/fixtures/classifier-gold/transcripts/04-feature-webhook-retry.txt +95 -0
- package/fixtures/classifier-gold/transcripts/05-feature-csv-export.txt +86 -0
- package/fixtures/classifier-gold/transcripts/06-feature-rate-limiting.txt +61 -0
- package/fixtures/classifier-gold/transcripts/07-refactor-db-abstraction.txt +72 -0
- package/fixtures/classifier-gold/transcripts/08-refactor-config-loading.txt +89 -0
- package/fixtures/classifier-gold/transcripts/09-ops-docker-staging.txt +94 -0
- package/fixtures/classifier-gold/transcripts/10-ops-nginx-ssl.txt +121 -0
- package/fixtures/classifier-gold/transcripts/11-ops-postgres-migration.txt +74 -0
- package/fixtures/classifier-gold/transcripts/12-ops-cron-debugging.txt +73 -0
- package/fixtures/classifier-gold/transcripts/13-research-vector-db-comparison.txt +59 -0
- package/fixtures/classifier-gold/transcripts/14-research-oauth-provider.txt +74 -0
- package/fixtures/classifier-gold/transcripts/15-writing-api-docs.txt +154 -0
- package/fixtures/classifier-gold/transcripts/16-meeting-sprint-planning.txt +69 -0
- package/fixtures/classifier-gold/transcripts/17-trivial-list-files.txt +14 -0
- package/fixtures/classifier-gold/transcripts/18-trivial-version-check.txt +21 -0
- package/fixtures/classifier-gold/transcripts/19-trivial-syntax-help.txt +29 -0
- package/fixtures/classifier-gold/transcripts/20-trivial-read-package-json.txt +41 -0
- package/migrations/000_initial_schema.sql +1 -1
- package/migrations/024_backfill_exemplar_repo_basename.sql +32 -0
- package/migrations/025_workstreams.sql +30 -0
- package/migrations/026_embedding_config.sql +12 -0
- package/migrations/027_drop_legacy_session_embeddings.sql +7 -0
- package/migrations/028_classifier_provenance.sql +5 -0
- package/migrations/pg/001_initial.sql +34 -0
- package/migrations/pg/019_split_replaces.sql +16 -7
- package/migrations/pg/025_workstreams.sql +34 -0
- package/migrations/pg/026_embedding_config.sql +12 -0
- package/migrations/pg/028_classifier_provenance.sql +5 -0
- package/migrations/pg/029_entity_variants.sql +9 -0
- package/nlm/index.js +395 -37
- package/package.json +12 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/prompt-recall-hook.mjs +402 -41
- package/plugin/scripts/session-start-hook.mjs +286 -60
- package/plugin/scripts/stop-hook.mjs +85 -65
- package/plugin-hermes-agent/__init__.py +2 -1
- package/templates/agent-contract/README.md +24 -0
- package/templates/agent-contract/claude-code.md +40 -0
- package/templates/agent-contract/generic.md +38 -0
- package/dist/core/embedding/embed-normalize.d.ts +0 -28
- package/dist/core/embedding/embed-normalize.js +0 -98
- package/dist/core/embedding/embed-normalize.js.map +0 -1
- package/dist/core/storage/pg-tx-context.d.ts +0 -52
- package/dist/core/storage/pg-tx-context.js +0 -149
- package/dist/core/storage/pg-tx-context.js.map +0 -1
|
@@ -21,7 +21,15 @@ function appendHookLog(entry) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// src/core/hook/memo.ts
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync as mkdirSync2,
|
|
27
|
+
readdirSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
rmSync,
|
|
30
|
+
statSync,
|
|
31
|
+
writeFileSync
|
|
32
|
+
} from "node:fs";
|
|
25
33
|
import { homedir as homedir2 } from "node:os";
|
|
26
34
|
import { join as join2 } from "node:path";
|
|
27
35
|
function stateDir() {
|
|
@@ -53,6 +61,26 @@ function recordSurfaced(conversationId, ids) {
|
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
// src/core/hook/pointer-block.ts
|
|
64
|
+
function truncateSummary(s, max = 200) {
|
|
65
|
+
if (s.length <= max) return s;
|
|
66
|
+
const window = s.slice(0, max);
|
|
67
|
+
const last = window[window.length - 1];
|
|
68
|
+
if ((last === "." || last === "!" || last === "?") && window.length - 1 >= 60) {
|
|
69
|
+
return window;
|
|
70
|
+
}
|
|
71
|
+
for (let i = window.length - 2; i >= 60; i--) {
|
|
72
|
+
const c = window[i];
|
|
73
|
+
if ((c === "." || c === "!" || c === "?") && window[i + 1] === " ") {
|
|
74
|
+
return s.slice(0, i + 1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (let i = window.length - 1; i >= 60; i--) {
|
|
78
|
+
if (window[i] === " ") {
|
|
79
|
+
return s.slice(0, i) + " ...";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return window + " ...";
|
|
83
|
+
}
|
|
56
84
|
function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
57
85
|
if (hits.length === 0 && facts.length === 0 && exemplars.length === 0) return "";
|
|
58
86
|
const out = [];
|
|
@@ -61,7 +89,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
61
89
|
for (const h of hits) {
|
|
62
90
|
const datePart = h.startedAt.slice(0, 10);
|
|
63
91
|
if (h.summary) {
|
|
64
|
-
out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${h.summary
|
|
92
|
+
out.push(`- ${h.id} \xB7 ${h.label} (${datePart}) \u2014 ${truncateSummary(h.summary)}`);
|
|
65
93
|
} else {
|
|
66
94
|
out.push(`- ${h.id} \xB7 ${h.label} (${datePart})`);
|
|
67
95
|
}
|
|
@@ -80,7 +108,7 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
80
108
|
out.push("## Related code exemplars (nlm-memory)");
|
|
81
109
|
for (const e of exemplars) {
|
|
82
110
|
const langPart = e.lang ? `${e.lang} \xB7 ` : "";
|
|
83
|
-
out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${e.taskContext
|
|
111
|
+
out.push(`- [${e.outcome}] ${langPart}${e.repo} - ${truncateSummary(e.taskContext)}`);
|
|
84
112
|
}
|
|
85
113
|
}
|
|
86
114
|
const tools = exemplars.length > 0 ? "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved), recall_code (pull the full code for a related exemplar)." : "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved).";
|
|
@@ -89,10 +117,16 @@ function formatPointerBlock(hits, facts = [], exemplars = []) {
|
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
// src/core/hook/select.ts
|
|
120
|
+
function medianScore(hits) {
|
|
121
|
+
if (hits.length === 0) return 0;
|
|
122
|
+
const sorted = hits.map((h) => h.matchScore).sort((a, b) => a - b);
|
|
123
|
+
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
124
|
+
}
|
|
92
125
|
function selectHits(params) {
|
|
93
|
-
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
|
|
126
|
+
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap, relativeFloor = 0 } = params;
|
|
127
|
+
const relCut = relativeFloor > 0 ? relativeFloor * medianScore(hits) : 0;
|
|
94
128
|
const eligible = hits.filter(
|
|
95
|
-
(h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id)
|
|
129
|
+
(h) => h.matchScore >= scoreThreshold && h.matchScore >= relCut && !surfaced.has(h.id)
|
|
96
130
|
);
|
|
97
131
|
const budget = Math.max(0, perConversationCap - surfaced.size);
|
|
98
132
|
const limit = Math.min(perFireCap, budget);
|
|
@@ -149,12 +183,240 @@ function hookAuthHeaders(extra = {}) {
|
|
|
149
183
|
return { ...extra, authorization: `Bearer ${token}` };
|
|
150
184
|
}
|
|
151
185
|
|
|
186
|
+
// src/hook/score-floor.ts
|
|
187
|
+
function parseScoreFloor(raw) {
|
|
188
|
+
if (raw === void 0) return 0;
|
|
189
|
+
const parsed = Number(raw);
|
|
190
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
|
191
|
+
return parsed;
|
|
192
|
+
}
|
|
193
|
+
function parseRelativeFloor(raw, fallback) {
|
|
194
|
+
if (raw === void 0) return fallback;
|
|
195
|
+
const parsed = Number(raw);
|
|
196
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/core/hook/query-extract.ts
|
|
201
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
202
|
+
"a",
|
|
203
|
+
"an",
|
|
204
|
+
"the",
|
|
205
|
+
"is",
|
|
206
|
+
"are",
|
|
207
|
+
"was",
|
|
208
|
+
"were",
|
|
209
|
+
"be",
|
|
210
|
+
"been",
|
|
211
|
+
"being",
|
|
212
|
+
"have",
|
|
213
|
+
"has",
|
|
214
|
+
"had",
|
|
215
|
+
"do",
|
|
216
|
+
"does",
|
|
217
|
+
"did",
|
|
218
|
+
"will",
|
|
219
|
+
"would",
|
|
220
|
+
"could",
|
|
221
|
+
"should",
|
|
222
|
+
"may",
|
|
223
|
+
"might",
|
|
224
|
+
"shall",
|
|
225
|
+
"can",
|
|
226
|
+
"dare",
|
|
227
|
+
"ought",
|
|
228
|
+
"yes",
|
|
229
|
+
"no",
|
|
230
|
+
"not",
|
|
231
|
+
"please",
|
|
232
|
+
"thank",
|
|
233
|
+
"thanks",
|
|
234
|
+
"ok",
|
|
235
|
+
"okay",
|
|
236
|
+
"i",
|
|
237
|
+
"me",
|
|
238
|
+
"my",
|
|
239
|
+
"we",
|
|
240
|
+
"us",
|
|
241
|
+
"our",
|
|
242
|
+
"you",
|
|
243
|
+
"your",
|
|
244
|
+
"it",
|
|
245
|
+
"its",
|
|
246
|
+
"this",
|
|
247
|
+
"that",
|
|
248
|
+
"these",
|
|
249
|
+
"those",
|
|
250
|
+
"and",
|
|
251
|
+
"or",
|
|
252
|
+
"but",
|
|
253
|
+
"if",
|
|
254
|
+
"so",
|
|
255
|
+
"to",
|
|
256
|
+
"of",
|
|
257
|
+
"in",
|
|
258
|
+
"on",
|
|
259
|
+
"at",
|
|
260
|
+
"by",
|
|
261
|
+
"for",
|
|
262
|
+
"from",
|
|
263
|
+
"with",
|
|
264
|
+
"about",
|
|
265
|
+
"into",
|
|
266
|
+
"through",
|
|
267
|
+
"during",
|
|
268
|
+
"before",
|
|
269
|
+
"after",
|
|
270
|
+
"above",
|
|
271
|
+
"below",
|
|
272
|
+
"up",
|
|
273
|
+
"down",
|
|
274
|
+
"out",
|
|
275
|
+
"off",
|
|
276
|
+
"over",
|
|
277
|
+
"under",
|
|
278
|
+
"again",
|
|
279
|
+
"further",
|
|
280
|
+
"then",
|
|
281
|
+
"once",
|
|
282
|
+
"here",
|
|
283
|
+
"there",
|
|
284
|
+
"when",
|
|
285
|
+
"where",
|
|
286
|
+
"why",
|
|
287
|
+
"how",
|
|
288
|
+
"all",
|
|
289
|
+
"both",
|
|
290
|
+
"each",
|
|
291
|
+
"few",
|
|
292
|
+
"more",
|
|
293
|
+
"most",
|
|
294
|
+
"other",
|
|
295
|
+
"some",
|
|
296
|
+
"such",
|
|
297
|
+
"than",
|
|
298
|
+
"too",
|
|
299
|
+
"very",
|
|
300
|
+
"just",
|
|
301
|
+
"now",
|
|
302
|
+
"also",
|
|
303
|
+
"get",
|
|
304
|
+
"let",
|
|
305
|
+
"what",
|
|
306
|
+
"which",
|
|
307
|
+
"who",
|
|
308
|
+
"whom",
|
|
309
|
+
"whose",
|
|
310
|
+
"any",
|
|
311
|
+
"much",
|
|
312
|
+
"many",
|
|
313
|
+
"sounds",
|
|
314
|
+
"good",
|
|
315
|
+
"great",
|
|
316
|
+
"sure",
|
|
317
|
+
"right",
|
|
318
|
+
"well",
|
|
319
|
+
"done",
|
|
320
|
+
"nice",
|
|
321
|
+
"cool",
|
|
322
|
+
"perfect",
|
|
323
|
+
"exactly",
|
|
324
|
+
"proceed",
|
|
325
|
+
"continue",
|
|
326
|
+
"go",
|
|
327
|
+
"ahead",
|
|
328
|
+
"next",
|
|
329
|
+
"help"
|
|
330
|
+
]);
|
|
331
|
+
var MIN_CONTENT_WORDS = 2;
|
|
332
|
+
var MIN_WORD_LEN = 3;
|
|
333
|
+
var SYSTEM_MESSAGE_PREFIX = /^<(task-notification|command-name|command-message|command-args|local-command-stdout|local-command-caveat|output-file|system-reminder)\b/;
|
|
334
|
+
function extractRecallQuery(prompt) {
|
|
335
|
+
if (SYSTEM_MESSAGE_PREFIX.test(prompt.trim())) return null;
|
|
336
|
+
const tokens = prompt.trim().split(/\s+/).map((t) => t.replace(/^[^\w-]+|[^\w-]+$/g, "")).filter((t) => t.length >= MIN_WORD_LEN);
|
|
337
|
+
const contentWords = tokens.filter((t) => !STOPWORDS.has(t.toLowerCase()));
|
|
338
|
+
if (contentWords.length < MIN_CONTENT_WORDS) return null;
|
|
339
|
+
return contentWords.join(" ");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/hook/hook-helpers.ts
|
|
343
|
+
function readStdin() {
|
|
344
|
+
return new Promise((resolve2) => {
|
|
345
|
+
let data = "";
|
|
346
|
+
process.stdin.setEncoding("utf8");
|
|
347
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
348
|
+
process.stdin.on("end", () => resolve2(data));
|
|
349
|
+
process.stdin.on("error", () => resolve2(data));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async function fetchWithTimeout(url, init, ms) {
|
|
353
|
+
const controller = new AbortController();
|
|
354
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
355
|
+
try {
|
|
356
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
357
|
+
} finally {
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function hookModeFromEnv() {
|
|
362
|
+
return process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/hook/recall-over-http.ts
|
|
366
|
+
var RECALL_LIMIT = 5;
|
|
367
|
+
var RECALL_TIMEOUT_MS = 2e3;
|
|
368
|
+
async function recallOverHttp(prompt, runtime, conversationId, mode = "keyword") {
|
|
369
|
+
const query = extractRecallQuery(prompt);
|
|
370
|
+
if (query === null) return { hits: [], facts: [], exemplars: [] };
|
|
371
|
+
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
372
|
+
const url = (
|
|
373
|
+
// 127.0.0.1, not localhost: each hook is a fresh process with no connection
|
|
374
|
+
// reuse, and Node resolves localhost to IPv6 ::1 first — a measured ~50-300ms
|
|
375
|
+
// per-fire connect penalty vs ~3ms on the explicit IPv4 loopback.
|
|
376
|
+
`http://127.0.0.1:${portValue}/api/recall?q=${encodeURIComponent(query)}&mode=${mode}&limit=${RECALL_LIMIT}&withFacts=true&withExemplars=true` + (conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "")
|
|
377
|
+
);
|
|
378
|
+
try {
|
|
379
|
+
const extra = { "x-recall-source": "hook" };
|
|
380
|
+
if (runtime) extra["x-recall-runtime"] = runtime;
|
|
381
|
+
const res = await fetchWithTimeout(url, { headers: hookAuthHeaders(extra) }, RECALL_TIMEOUT_MS);
|
|
382
|
+
if (!res.ok) return { hits: [], facts: [], exemplars: [] };
|
|
383
|
+
let body;
|
|
384
|
+
try {
|
|
385
|
+
body = await res.json();
|
|
386
|
+
} catch {
|
|
387
|
+
return { hits: [], facts: [], exemplars: [] };
|
|
388
|
+
}
|
|
389
|
+
const hits = (body.results ?? []).map((r) => ({
|
|
390
|
+
id: r.id,
|
|
391
|
+
label: r.label,
|
|
392
|
+
startedAt: r.startedAt,
|
|
393
|
+
matchScore: r.matchScore,
|
|
394
|
+
...r.summary !== void 0 ? { summary: r.summary } : {}
|
|
395
|
+
}));
|
|
396
|
+
const facts = (body.relatedFacts ?? []).map((f) => ({
|
|
397
|
+
subject: f.subject,
|
|
398
|
+
predicate: f.predicate,
|
|
399
|
+
value: f.value,
|
|
400
|
+
corroborationCount: f.corroborationCount
|
|
401
|
+
}));
|
|
402
|
+
const exemplars = (body.relatedExemplars ?? []).map((e) => ({
|
|
403
|
+
outcome: e.outcome,
|
|
404
|
+
lang: e.lang,
|
|
405
|
+
repo: e.repo,
|
|
406
|
+
taskContext: e.taskContext
|
|
407
|
+
}));
|
|
408
|
+
return { hits, facts, exemplars };
|
|
409
|
+
} catch {
|
|
410
|
+
return { hits: [], facts: [], exemplars: [] };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
152
414
|
// src/hook/session-start-hook.ts
|
|
153
|
-
var SCORE_THRESHOLD =
|
|
415
|
+
var SCORE_THRESHOLD = parseScoreFloor(process.env["NLM_RECALL_SCORE_FLOOR"]);
|
|
416
|
+
var RELATIVE_FLOOR = parseRelativeFloor(process.env["NLM_RECALL_REL_FLOOR"], 0.9);
|
|
154
417
|
var PER_FIRE_CAP = 3;
|
|
155
418
|
var PER_CONVERSATION_CAP = 10;
|
|
156
|
-
var
|
|
157
|
-
var RECALL_TIMEOUT_MS = 2e3;
|
|
419
|
+
var RECALL_TIMEOUT_MS2 = 2e3;
|
|
158
420
|
async function runHook(input, deps) {
|
|
159
421
|
let hits = [];
|
|
160
422
|
try {
|
|
@@ -167,6 +429,7 @@ async function runHook(input, deps) {
|
|
|
167
429
|
hits,
|
|
168
430
|
surfaced,
|
|
169
431
|
scoreThreshold: SCORE_THRESHOLD,
|
|
432
|
+
relativeFloor: RELATIVE_FLOOR,
|
|
170
433
|
perFireCap: PER_FIRE_CAP,
|
|
171
434
|
perConversationCap: PER_CONVERSATION_CAP
|
|
172
435
|
});
|
|
@@ -194,21 +457,16 @@ function composeSessionStartOutput(failureModeBlock, recallBlock) {
|
|
|
194
457
|
async function fetchFailureModeBlock(repo) {
|
|
195
458
|
if (!repo) return "";
|
|
196
459
|
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
197
|
-
const url = `http://
|
|
198
|
-
const controller = new AbortController();
|
|
199
|
-
const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
|
|
460
|
+
const url = `http://127.0.0.1:${portValue}/api/signals/failure-modes?repo=${encodeURIComponent(repo)}`;
|
|
200
461
|
try {
|
|
201
|
-
const res = await
|
|
202
|
-
headers: hookAuthHeaders({ "x-recall-source": "session-start-hook" })
|
|
203
|
-
|
|
204
|
-
});
|
|
462
|
+
const res = await fetchWithTimeout(url, {
|
|
463
|
+
headers: hookAuthHeaders({ "x-recall-source": "session-start-hook" })
|
|
464
|
+
}, RECALL_TIMEOUT_MS2);
|
|
205
465
|
if (!res.ok) return "";
|
|
206
466
|
const body = await res.json();
|
|
207
467
|
return typeof body.block === "string" ? body.block : "";
|
|
208
468
|
} catch {
|
|
209
469
|
return "";
|
|
210
|
-
} finally {
|
|
211
|
-
clearTimeout(timer);
|
|
212
470
|
}
|
|
213
471
|
}
|
|
214
472
|
function buildQuery(workingDirectory, projectName) {
|
|
@@ -216,46 +474,6 @@ function buildQuery(workingDirectory, projectName) {
|
|
|
216
474
|
const parts = [dirTail, projectName].filter(Boolean);
|
|
217
475
|
return parts.join(" ").trim() || "session start";
|
|
218
476
|
}
|
|
219
|
-
function readStdin() {
|
|
220
|
-
return new Promise((resolve2) => {
|
|
221
|
-
let data = "";
|
|
222
|
-
process.stdin.setEncoding("utf8");
|
|
223
|
-
process.stdin.on("data", (chunk) => data += chunk);
|
|
224
|
-
process.stdin.on("end", () => resolve2(data));
|
|
225
|
-
process.stdin.on("error", () => resolve2(data));
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
async function recallOverHttp(query, conversationId) {
|
|
229
|
-
const portValue = process.env["NLM_PORT"] ?? "3940";
|
|
230
|
-
const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(query)}&mode=hybrid&limit=${RECALL_LIMIT}` + (conversationId ? `&conversation_id=${encodeURIComponent(conversationId)}` : "");
|
|
231
|
-
const controller = new AbortController();
|
|
232
|
-
const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
|
|
233
|
-
try {
|
|
234
|
-
const res = await fetch(url, {
|
|
235
|
-
headers: hookAuthHeaders({
|
|
236
|
-
"x-recall-source": "session-start-hook",
|
|
237
|
-
"x-recall-runtime": "claude-code"
|
|
238
|
-
}),
|
|
239
|
-
signal: controller.signal
|
|
240
|
-
});
|
|
241
|
-
if (!res.ok) return [];
|
|
242
|
-
let body;
|
|
243
|
-
try {
|
|
244
|
-
body = await res.json();
|
|
245
|
-
} catch {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
return (body.results ?? []).map((r) => ({
|
|
249
|
-
id: r.id,
|
|
250
|
-
label: r.label,
|
|
251
|
-
startedAt: r.startedAt,
|
|
252
|
-
matchScore: r.matchScore,
|
|
253
|
-
...r.summary !== void 0 ? { summary: r.summary } : {}
|
|
254
|
-
}));
|
|
255
|
-
} finally {
|
|
256
|
-
clearTimeout(timer);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
477
|
async function main() {
|
|
260
478
|
try {
|
|
261
479
|
autoloadEnv();
|
|
@@ -265,9 +483,17 @@ async function main() {
|
|
|
265
483
|
const workingDirectory = typeof payload.cwd === "string" ? payload.cwd : typeof payload.working_directory === "string" ? payload.working_directory : "";
|
|
266
484
|
const projectName = typeof payload.project_name === "string" ? payload.project_name : "";
|
|
267
485
|
const query = buildQuery(workingDirectory, projectName);
|
|
268
|
-
const mode =
|
|
269
|
-
const out = await
|
|
270
|
-
|
|
486
|
+
const mode = hookModeFromEnv();
|
|
487
|
+
const [out, failureModes] = await Promise.all([
|
|
488
|
+
runHook(
|
|
489
|
+
{ conversationId, query },
|
|
490
|
+
{
|
|
491
|
+
mode,
|
|
492
|
+
recall: async (q, cid) => (await recallOverHttp(q, "claude-code", cid === "unknown" ? void 0 : cid, "hybrid")).hits
|
|
493
|
+
}
|
|
494
|
+
),
|
|
495
|
+
mode === "live" ? fetchFailureModeBlock(workingDirectory) : Promise.resolve("")
|
|
496
|
+
]);
|
|
271
497
|
const combined = composeSessionStartOutput(failureModes, out);
|
|
272
498
|
if (combined) process.stdout.write(combined);
|
|
273
499
|
} catch {
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
// src/hook/stop-hook.ts
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
6
|
-
import { homedir as homedir5 } from "node:os";
|
|
7
|
-
import { dirname as dirname2, join as join4 } from "node:path";
|
|
8
5
|
|
|
9
6
|
// src/core/hook/citation-detect.ts
|
|
10
7
|
var MIN_ID_LEN = 6;
|
|
@@ -96,7 +93,15 @@ function extractId(input) {
|
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
// src/core/hook/memo.ts
|
|
99
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
existsSync,
|
|
98
|
+
mkdirSync,
|
|
99
|
+
readdirSync,
|
|
100
|
+
readFileSync,
|
|
101
|
+
rmSync,
|
|
102
|
+
statSync,
|
|
103
|
+
writeFileSync
|
|
104
|
+
} from "node:fs";
|
|
100
105
|
import { homedir } from "node:os";
|
|
101
106
|
import { join } from "node:path";
|
|
102
107
|
function stateDir() {
|
|
@@ -163,10 +168,10 @@ function isEnabled() {
|
|
|
163
168
|
if (raw === void 0) return true;
|
|
164
169
|
return raw !== "0" && raw.toLowerCase() !== "false";
|
|
165
170
|
}
|
|
166
|
-
async function appendMisses(entries,
|
|
171
|
+
async function appendMisses(entries, logPath) {
|
|
167
172
|
if (!isEnabled()) return;
|
|
168
173
|
if (entries.length === 0) return;
|
|
169
|
-
const path =
|
|
174
|
+
const path = logPath ?? defaultLogPath();
|
|
170
175
|
try {
|
|
171
176
|
await mkdir(dirname(path), { recursive: true });
|
|
172
177
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -178,7 +183,7 @@ async function appendMisses(entries, logPath2) {
|
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
// src/core/hook/transcript.ts
|
|
181
|
-
import { existsSync as existsSync3,
|
|
186
|
+
import { closeSync, existsSync as existsSync3, fstatSync, openSync, readSync } from "node:fs";
|
|
182
187
|
function parseTurn(parsed) {
|
|
183
188
|
if (parsed.type !== "assistant" || !parsed.message) return null;
|
|
184
189
|
const content = parsed.message.content;
|
|
@@ -198,10 +203,22 @@ function parseTurn(parsed) {
|
|
|
198
203
|
if (textParts.length === 0 && toolUses.length === 0) return null;
|
|
199
204
|
return { text: textParts.join("\n"), toolUses };
|
|
200
205
|
}
|
|
206
|
+
var MAX_TRANSCRIPT_BYTES = 256 * 1024;
|
|
201
207
|
function readLines(transcriptPath) {
|
|
202
208
|
if (!transcriptPath || !existsSync3(transcriptPath)) return null;
|
|
203
209
|
try {
|
|
204
|
-
|
|
210
|
+
const fd = openSync(transcriptPath, "r");
|
|
211
|
+
try {
|
|
212
|
+
const size = fstatSync(fd).size;
|
|
213
|
+
const start = size > MAX_TRANSCRIPT_BYTES ? size - MAX_TRANSCRIPT_BYTES : 0;
|
|
214
|
+
const len = size - start;
|
|
215
|
+
if (len <= 0) return [];
|
|
216
|
+
const buf = Buffer.alloc(len);
|
|
217
|
+
readSync(fd, buf, 0, len, start);
|
|
218
|
+
return buf.toString("utf8").split("\n");
|
|
219
|
+
} finally {
|
|
220
|
+
closeSync(fd);
|
|
221
|
+
}
|
|
205
222
|
} catch {
|
|
206
223
|
return null;
|
|
207
224
|
}
|
|
@@ -226,7 +243,7 @@ function readAllAssistantTurns(transcriptPath) {
|
|
|
226
243
|
}
|
|
227
244
|
|
|
228
245
|
// src/llm/env-autoload.ts
|
|
229
|
-
import { readFileSync as
|
|
246
|
+
import { readFileSync as readFileSync3, existsSync as existsSync4 } from "node:fs";
|
|
230
247
|
import { homedir as homedir4 } from "node:os";
|
|
231
248
|
import { resolve } from "node:path";
|
|
232
249
|
var DEFAULT_SEARCH_PATHS = [
|
|
@@ -246,7 +263,7 @@ function autoloadEnv(extraPaths = []) {
|
|
|
246
263
|
const path = expandHome(raw);
|
|
247
264
|
if (!existsSync4(path)) continue;
|
|
248
265
|
try {
|
|
249
|
-
const content =
|
|
266
|
+
const content = readFileSync3(path, "utf8");
|
|
250
267
|
for (const line of content.split("\n")) {
|
|
251
268
|
const trimmed = line.trim();
|
|
252
269
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -275,6 +292,41 @@ function hookAuthHeaders(extra = {}) {
|
|
|
275
292
|
return { ...extra, authorization: `Bearer ${token}` };
|
|
276
293
|
}
|
|
277
294
|
|
|
295
|
+
// src/hook/hook-helpers.ts
|
|
296
|
+
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
297
|
+
import { homedir as homedir5 } from "node:os";
|
|
298
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
299
|
+
function readStdin() {
|
|
300
|
+
return new Promise((resolve2) => {
|
|
301
|
+
let data = "";
|
|
302
|
+
process.stdin.setEncoding("utf8");
|
|
303
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
304
|
+
process.stdin.on("end", () => resolve2(data));
|
|
305
|
+
process.stdin.on("error", () => resolve2(data));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async function fetchWithTimeout(url, init, ms) {
|
|
309
|
+
const controller = new AbortController();
|
|
310
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
311
|
+
try {
|
|
312
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
313
|
+
} finally {
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function hookModeFromEnv() {
|
|
318
|
+
return process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
|
|
319
|
+
}
|
|
320
|
+
function appendHookEvent(data) {
|
|
321
|
+
try {
|
|
322
|
+
const path = process.env["NLM_HOOK_LOG"] ?? join4(homedir5(), ".nlm", "hook-log.jsonl");
|
|
323
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
324
|
+
appendFileSync(path, `${JSON.stringify(data)}
|
|
325
|
+
`, "utf8");
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
278
330
|
// src/hook/stop-hook.ts
|
|
279
331
|
var RESPONSE_PREVIEW_CHARS = 200;
|
|
280
332
|
var POST_TIMEOUT_MS = 1500;
|
|
@@ -335,12 +387,9 @@ async function runStopHook(input, deps) {
|
|
|
335
387
|
}
|
|
336
388
|
const lastText = turns[turns.length - 1]?.text ?? "";
|
|
337
389
|
const preview = lastText.slice(0, RESPONSE_PREVIEW_CHARS);
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
} catch {
|
|
342
|
-
}
|
|
343
|
-
}
|
|
390
|
+
await Promise.allSettled(
|
|
391
|
+
fresh.map((c) => deps.postCitation(input.conversationId, c.id, c.kind, preview))
|
|
392
|
+
);
|
|
344
393
|
if (fresh.length > 0) {
|
|
345
394
|
recordCited(input.conversationId, fresh.map((c) => c.id));
|
|
346
395
|
}
|
|
@@ -352,60 +401,31 @@ async function runStopHook(input, deps) {
|
|
|
352
401
|
skipped: false
|
|
353
402
|
};
|
|
354
403
|
}
|
|
355
|
-
function logPath() {
|
|
356
|
-
return process.env["NLM_HOOK_LOG"] ?? join4(homedir5(), ".nlm", "hook-log.jsonl");
|
|
357
|
-
}
|
|
358
404
|
function logStopResult(result) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
surfacedCount: result.surfacedCount,
|
|
369
|
-
citedIds: result.citations.map((c) => c.id),
|
|
370
|
-
citationKinds: result.citations.map((c) => c.kind),
|
|
371
|
-
skipped: result.skipped,
|
|
372
|
-
mode: process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow"
|
|
373
|
-
})}
|
|
374
|
-
`,
|
|
375
|
-
"utf8"
|
|
376
|
-
);
|
|
377
|
-
} catch {
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
function readStdin() {
|
|
381
|
-
return new Promise((resolve2) => {
|
|
382
|
-
let data = "";
|
|
383
|
-
process.stdin.setEncoding("utf8");
|
|
384
|
-
process.stdin.on("data", (chunk) => data += chunk);
|
|
385
|
-
process.stdin.on("end", () => resolve2(data));
|
|
386
|
-
process.stdin.on("error", () => resolve2(data));
|
|
405
|
+
appendHookEvent({
|
|
406
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
407
|
+
kind: "stop",
|
|
408
|
+
conversationId: result.conversationId,
|
|
409
|
+
surfacedCount: result.surfacedCount,
|
|
410
|
+
citedIds: result.citations.map((c) => c.id),
|
|
411
|
+
citationKinds: result.citations.map((c) => c.kind),
|
|
412
|
+
skipped: result.skipped,
|
|
413
|
+
mode: hookModeFromEnv()
|
|
387
414
|
});
|
|
388
415
|
}
|
|
389
416
|
async function postCitationOverHttp(conversationId, citedId, kind, responsePreview) {
|
|
390
417
|
const port = process.env["NLM_PORT"] ?? "3940";
|
|
391
|
-
const url = `http://
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
response_preview: responsePreview
|
|
403
|
-
}),
|
|
404
|
-
signal: controller.signal
|
|
405
|
-
});
|
|
406
|
-
} finally {
|
|
407
|
-
clearTimeout(timer);
|
|
408
|
-
}
|
|
418
|
+
const url = `http://127.0.0.1:${port}/api/recall/cite-event`;
|
|
419
|
+
await fetchWithTimeout(url, {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: hookAuthHeaders({ "content-type": "application/json" }),
|
|
422
|
+
body: JSON.stringify({
|
|
423
|
+
...conversationId !== "unknown" ? { conversation_id: conversationId } : {},
|
|
424
|
+
cited_id: citedId,
|
|
425
|
+
kind,
|
|
426
|
+
response_preview: responsePreview
|
|
427
|
+
})
|
|
428
|
+
}, POST_TIMEOUT_MS);
|
|
409
429
|
}
|
|
410
430
|
async function main() {
|
|
411
431
|
try {
|
|
@@ -9,6 +9,7 @@ The daemon must be running for hooks to have effect. If it is unreachable,
|
|
|
9
9
|
every hook silently returns None so the agent loop is never blocked.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import http.client
|
|
12
13
|
import json
|
|
13
14
|
import os
|
|
14
15
|
import urllib.error
|
|
@@ -33,7 +34,7 @@ def _post(path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
33
34
|
try:
|
|
34
35
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
35
36
|
return json.loads(resp.read())
|
|
36
|
-
except (urllib.error.URLError, OSError, json.JSONDecodeError):
|
|
37
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError, http.client.HTTPException):
|
|
37
38
|
return {}
|
|
38
39
|
|
|
39
40
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Agent Contract Templates
|
|
2
|
+
|
|
3
|
+
These templates teach an agent the recall contract for working effectively with NLM Memory.
|
|
4
|
+
|
|
5
|
+
## Variants
|
|
6
|
+
|
|
7
|
+
- `claude-code.md` - CLAUDE.md-ready markdown section (heading + rules). Drop it into any Claude Code project's CLAUDE.md, or use `nlm init --agent claude-code --write CLAUDE.md`.
|
|
8
|
+
- `generic.md` - Tool-agnostic prose for any agent instruction file. Use `nlm init --agent generic --write <path>`.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
# Print to stdout
|
|
14
|
+
nlm init --agent claude-code
|
|
15
|
+
nlm init --agent generic
|
|
16
|
+
|
|
17
|
+
# Append to a file (adds begin/end markers; refuses if markers already exist)
|
|
18
|
+
nlm init --agent claude-code --write CLAUDE.md
|
|
19
|
+
|
|
20
|
+
# Replace the block in place when markers already exist
|
|
21
|
+
nlm init --agent claude-code --write CLAUDE.md --force
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The block is delimited by `<!-- nlm-agent-contract:begin -->` and `<!-- nlm-agent-contract:end -->` so re-runs detect it and refuse to duplicate without `--force`.
|