mnemon-mcp 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +68 -19
  3. package/dist/date-extractor.d.ts +30 -0
  4. package/dist/date-extractor.d.ts.map +1 -0
  5. package/dist/date-extractor.js +185 -0
  6. package/dist/date-extractor.js.map +1 -0
  7. package/dist/db.d.ts.map +1 -1
  8. package/dist/db.js +264 -220
  9. package/dist/db.js.map +1 -1
  10. package/dist/embedder.d.ts +1 -0
  11. package/dist/embedder.d.ts.map +1 -1
  12. package/dist/embedder.js.map +1 -1
  13. package/dist/import/embed-backfill.d.ts +21 -0
  14. package/dist/import/embed-backfill.d.ts.map +1 -0
  15. package/dist/import/embed-backfill.js +187 -0
  16. package/dist/import/embed-backfill.js.map +1 -0
  17. package/dist/import/kb-import.d.ts.map +1 -1
  18. package/dist/import/kb-import.js +5 -2
  19. package/dist/import/kb-import.js.map +1 -1
  20. package/dist/import/md-parser.d.ts +2 -0
  21. package/dist/import/md-parser.d.ts.map +1 -1
  22. package/dist/import/md-parser.js +17 -0
  23. package/dist/import/md-parser.js.map +1 -1
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +39 -3
  26. package/dist/server.js.map +1 -1
  27. package/dist/stemmer.d.ts +0 -11
  28. package/dist/stemmer.d.ts.map +1 -1
  29. package/dist/stemmer.js +28 -2
  30. package/dist/stemmer.js.map +1 -1
  31. package/dist/stop-words.d.ts.map +1 -1
  32. package/dist/stop-words.js +4 -0
  33. package/dist/stop-words.js.map +1 -1
  34. package/dist/tools/memory-add.d.ts.map +1 -1
  35. package/dist/tools/memory-add.js +10 -4
  36. package/dist/tools/memory-add.js.map +1 -1
  37. package/dist/tools/memory-health.d.ts.map +1 -1
  38. package/dist/tools/memory-health.js +23 -4
  39. package/dist/tools/memory-health.js.map +1 -1
  40. package/dist/tools/memory-search.d.ts.map +1 -1
  41. package/dist/tools/memory-search.js +309 -66
  42. package/dist/tools/memory-search.js.map +1 -1
  43. package/dist/tools/session.d.ts +13 -0
  44. package/dist/tools/session.d.ts.map +1 -0
  45. package/dist/tools/session.js +78 -0
  46. package/dist/tools/session.js.map +1 -0
  47. package/dist/types.d.ts +40 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/validation.d.ts +19 -1
  50. package/dist/validation.d.ts.map +1 -1
  51. package/dist/validation.js +19 -1
  52. package/dist/validation.js.map +1 -1
  53. package/package.json +12 -4
  54. package/server.json +16 -47
  55. package/dist/.tsbuildinfo +0 -1
  56. package/dist/tools/style-extract.d.ts +0 -40
  57. package/dist/tools/style-extract.d.ts.map +0 -1
  58. package/dist/tools/style-extract.js +0 -43
  59. package/dist/tools/style-extract.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2026-03-18
11
+
12
+ ### Added
13
+ - **Hybrid search** — FTS5 + vector via Reciprocal Rank Fusion (RRF, k=60). Auto-enabled when embedding provider is configured. L2 eval: 92.6/100 (up from 80.9 FTS-only)
14
+ - **Structured date extraction** — Russian natural language dates ("3 марта 2026", "в феврале–марте 2025") auto-parsed into SQL WHERE filters with query term stripping
15
+ - **Embedding backfill CLI** — `npm run embed:backfill` batch-embeds active memories via configured provider (OpenAI/Ollama). Supports `--force`, `--batch-size`, text truncation for token limits
16
+ - **Section-level `event_at` extraction** — import pipeline now extracts dates from H2/H3 section titles, not just filenames
17
+ - **Cross-reference query expansion** — queries mentioning entities expand to include related terms for better recall
18
+ - **Bilingual month expansion** — EN↔RU month names added at both index and query time ("march" matches "марта" and vice versa)
19
+ - **Document-type stop words** — "дневник", "журнал", "запись" forms filtered from FTS queries to reduce noise
20
+ - `source_file` field in `memory_search` response — enables eval file matching and provenance tracking
21
+ - `embedding_model` column (migration v6) — tracks which model embedded each memory
22
+ - 33 new tests: 6 md-parser + 27 date-extractor + 2 integration (229 total)
23
+
24
+ ### Fixed
25
+ - **BM25 score inversion** — `1/(1+|rank|)` inverted relevance ordering; corrected to `|rank|/(1+|rank|)` (+9 L2 points)
26
+ - **Single-quote FTS5 crash** — queries with apostrophes now escaped before MATCH
27
+ - **ё→е normalization** — "ёлка" now matches "елка" in both indexing and search
28
+ - **AND relaxation tuning** — relaxed from top-3 stems at 4+ tokens to top-2 at 3+, improving recall on short queries
29
+ - **session_id validation** — `memory_add` now verifies session_id FK exists before insert
30
+ - **Error sanitization** — tool errors return generic messages to MCP clients; full stack traces logged to stderr only
31
+ - **Date validation** — `isoDatePrefix` now rejects structurally invalid dates (month 13, day 00) via `Date.parse` refine
32
+ - **`search_log` retention** — probabilistic pruning removes entries older than 90 days (~1% of writes)
33
+ - Removed stale `dist/tools/style-extract.*` build artifacts from npm package
34
+ - Excluded `dist/.tsbuildinfo` (79 KB) from npm package
35
+
36
+ ## [1.1.0] - 2026-03-17
37
+
38
+ ### Added
39
+ - **Session lifecycle tools**: `memory_session_start`, `memory_session_end`, `memory_session_list` — group episodic memories by agent session with client, project, and summary tracking
40
+ - **Search query logging** — `search_log` table (migration v5) for query observability: query text, mode, result count, duration
41
+ - **Recency boost** in FTS scoring: `1 / (1 + daysSince / 365)` rewards recently created memories
42
+ - **Query-centered snippets** — search results highlight the first matched term instead of always starting from content beginning
43
+ - 12 new integration tests for session lifecycle (194 total: 17 md-parser + 13 kb-import + 123 integration + 41 validation)
44
+
45
+ ### Fixed
46
+ - Pagination with `min_confidence`/`min_importance` filters: moved from JS post-filter to SQL WHERE (fixes empty pages at high offsets)
47
+ - `memory_health` cleanup: chain repair now correctly reactivates predecessors when superseding entry is deleted
48
+ - Contradiction detection: properly handles edge case where superseded memory matches source_file
49
+ - Removed dead `conflicting` variable in memory-add
50
+
51
+ ## [1.0.1] - 2026-03-16
52
+
53
+ ### Added
54
+ - MCP Registry manifest (`server.json`) for official registry submission
55
+ - `mcpName` field in package.json for registry verification
56
+ - Landing page link in README
57
+ - Demo GIF with VHS recording scripts
58
+
59
+ ### Changed
60
+ - Homepage URL updated to landing page (aisatisfy.me/mnemon/)
61
+
10
62
  ## [1.0.0] - 2026-03-16
11
63
 
12
64
  ### Added
@@ -30,5 +82,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
30
82
  - 182 tests (unit + integration + validation)
31
83
  - CI pipeline with build, test, and smoke tests
32
84
 
33
- [Unreleased]: https://github.com/nikitacometa/mnemon-mcp/compare/v1.0.0...HEAD
85
+ [Unreleased]: https://github.com/nikitacometa/mnemon-mcp/compare/v1.2.0...HEAD
86
+ [1.2.0]: https://github.com/nikitacometa/mnemon-mcp/compare/v1.1.0...v1.2.0
87
+ [1.1.0]: https://github.com/nikitacometa/mnemon-mcp/compare/v1.0.1...v1.1.0
88
+ [1.0.1]: https://github.com/nikitacometa/mnemon-mcp/compare/v1.0.0...v1.0.1
34
89
  [1.0.0]: https://github.com/nikitacometa/mnemon-mcp/releases/tag/v1.0.0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/nikitacometa/mnemon-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/nikitacometa/mnemon-mcp/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/mnemon-mcp)](https://www.npmjs.com/package/mnemon-mcp)
5
- [![Node.js](https://img.shields.io/badge/node-%E2%89%A522-brightgreen)](https://nodejs.org/)
5
+ [![Node.js](https://img.shields.io/badge/node-%E2%89%A520-brightgreen)](https://nodejs.org/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
 
8
8
  **Persistent layered memory for AI agents.**
@@ -127,13 +127,13 @@ Use the full path to the compiled entry point:
127
127
  echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | mnemon-mcp
128
128
  ```
129
129
 
130
- You should see 7 tools in the response. The database (`~/.mnemon-mcp/memory.db`) is created automatically on first run.
130
+ You should see 10 tools in the response. The database (`~/.mnemon-mcp/memory.db`) is created automatically on first run.
131
131
 
132
132
  That's it. Your agent now has persistent memory.
133
133
 
134
134
  ## What It Can Do
135
135
 
136
- ### 7 MCP Tools
136
+ ### 10 MCP Tools
137
137
 
138
138
  | Tool | What it does |
139
139
  |------|-------------|
@@ -144,6 +144,9 @@ That's it. Your agent now has persistent memory.
144
144
  | **`memory_inspect`** | Get layer statistics or trace a single memory's version history |
145
145
  | **`memory_export`** | Export to JSON, Markdown, or Claude-md format with filters |
146
146
  | **`memory_health`** | Run diagnostics: expired entries, orphaned chains, stale memories; optionally GC |
147
+ | **`memory_session_start`** | Start an agent session — returns session ID for grouping memories |
148
+ | **`memory_session_end`** | End a session with optional summary; returns duration and memory count |
149
+ | **`memory_session_list`** | List sessions with filters by client, project, or active status |
147
150
 
148
151
  ### MCP Resources & Prompts
149
152
 
@@ -166,14 +169,20 @@ That's it. Your agent now has persistent memory.
166
169
 
167
170
  ## Search
168
171
 
169
- Two modes, both supporting layer / entity / scope / date / confidence filters:
172
+ Four modes, all supporting layer / entity / scope / date / confidence filters:
170
173
 
171
- **FTS mode** (default) — tokenized full-text search with BM25 ranking. Multi-word queries use AND; if too few results, OR supplements with a score penalty. Progressive AND relaxation tries top-3 most specific terms before falling back to full OR.
174
+ **FTS mode** (default without embeddings) — tokenized full-text search with BM25 ranking. Multi-word queries use AND; if too few results, OR supplements with a score penalty. Progressive AND relaxation tries top-3 most specific terms before falling back to full OR.
172
175
 
173
- Scores: `bm25 × (0.3 + 0.7 × importance) × decay(layer)`
176
+ **Hybrid mode** (default when embeddings configured) — combines FTS5 + vector search via [Reciprocal Rank Fusion](https://www.singlestore.com/blog/hybrid-search-using-reciprocal-rank-fusion-in-sql/). Detects quoted entities in queries (e.g., `'Essentialism'`) and runs weighted sub-queries for cross-reference retrieval.
177
+
178
+ **Vector mode** — pure cosine similarity search over embeddings.
174
179
 
175
180
  **Exact mode** — `LIKE` substring match for precise phrase lookups.
176
181
 
182
+ Scores: `bm25 × (0.3 + 0.7 × importance) × decay(layer) × recency`
183
+
184
+ Recency boost: `1 / (1 + daysSince / 365)` — gently rewards recently created memories without penalizing old ones.
185
+
177
186
  ### Stemming
178
187
 
179
188
  Snowball stemmer applied at both **index time** and **query time** for English and Russian. This means `"running"` matches `"runs"`, and `"книги"` matches `"книга"`. Stop words are filtered from queries to improve precision.
@@ -398,27 +407,67 @@ Returns: status (`healthy` / `warning` / `degraded`), per-layer stats, expired e
398
407
 
399
408
  </details>
400
409
 
410
+ <details>
411
+ <summary><code>memory_session_start</code></summary>
412
+
413
+ | Parameter | Type | Required | Description |
414
+ |-----------|------|----------|-------------|
415
+ | `client` | string | Yes | Client identifier (e.g. `claude-code`, `cursor`, `api`) |
416
+ | `project` | string | No | Project scope for this session |
417
+ | `meta` | object | No | Additional session metadata |
418
+
419
+ Returns: `id` (session UUID), `started_at` (ISO 8601).
420
+
421
+ </details>
422
+
423
+ <details>
424
+ <summary><code>memory_session_end</code></summary>
425
+
426
+ | Parameter | Type | Required | Description |
427
+ |-----------|------|----------|-------------|
428
+ | `id` | string | Yes | Session ID to end |
429
+ | `summary` | string | No | Summary of what was accomplished (max 10K chars) |
430
+
431
+ Returns: `id`, `ended_at`, `duration_minutes`, `memories_count`.
432
+
433
+ </details>
434
+
435
+ <details>
436
+ <summary><code>memory_session_list</code></summary>
437
+
438
+ | Parameter | Type | Required | Description |
439
+ |-----------|------|----------|-------------|
440
+ | `limit` | number | No | Max sessions (default 20, max 100) |
441
+ | `client` | string | No | Filter by client |
442
+ | `project` | string | No | Filter by project |
443
+ | `active_only` | boolean | No | Only return sessions that haven't ended (default false) |
444
+
445
+ Returns: array of sessions with `id`, `client`, `project`, `started_at`, `ended_at`, `summary`, `memories_count`.
446
+
447
+ </details>
448
+
401
449
  ## How It Compares
402
450
 
403
- | | **mnemon-mcp** | mem0 | basic-memory | Anthropic KG |
404
- |---|---|---|---|---|
405
- | **Architecture** | SQLite FTS5 | Cloud API + Qdrant | Markdown + vector | JSON file |
406
- | **Memory structure** | 4 typed layers | Flat | Flat | Graph |
407
- | **Fact versioning** | Superseding chains | Partial | No | No |
408
- | **Stemming** | EN + RU (Snowball) | EN only | EN only | None |
409
- | **OpenClaw support** | Native MCP | No | No | No |
410
- | **Dependencies** | 0 required | Qdrant, Neo4j, Ollama | FastEmbed, Python 3.12 | None |
411
- | **Cloud required** | No | Yes | No (SaaS optional) | No |
412
- | **Cost** | Free | $19–249/mo | Free + SaaS | Free |
413
- | **Setup** | `npm install -g` | Docker + API keys | pip + deps | Built-in |
414
- | **License** | MIT | Apache 2.0 | AGPL | MIT |
451
+ | | **mnemon-mcp** | mem0 | basic-memory | Engram | Anthropic KG |
452
+ |---|---|---|---|---|---|
453
+ | **Architecture** | SQLite FTS5 + vector | Cloud API + Qdrant | Markdown + vector | SQLite FTS5 | JSON file |
454
+ | **Memory structure** | 4 typed layers | Flat | Flat | Flat + sessions | Graph |
455
+ | **Search** | FTS5 + hybrid RRF | Semantic | Hybrid | FTS5 | Exact |
456
+ | **Fact versioning** | Superseding chains | Partial | No | No | No |
457
+ | **Stemming** | EN + RU (Snowball) | EN only | EN only | None | None |
458
+ | **Embeddings** | BYOK (OpenAI / Ollama) | Built-in | FastEmbed | None | None |
459
+ | **Dependencies** | 0 required | Qdrant, Neo4j | Python 3.12 | Go binary | None |
460
+ | **Cloud required** | No | Yes | No | No | No |
461
+ | **Cost** | Free | $19–249/mo | Free | Free | Free |
462
+ | **Setup** | `npm install -g` | Docker + API keys | pip + deps | Go install | Built-in |
463
+ | **License** | MIT | Apache 2.0 | AGPL | MIT | MIT |
415
464
 
416
465
  ## Development
417
466
 
418
467
  ```bash
419
468
  npm run dev # run via tsx (no build step)
420
469
  npm run build # TypeScript → dist/
421
- npm test # vitest (182 tests)
470
+ npm test # vitest (229 tests)
422
471
  npm run bench # performance benchmarks
423
472
  npm run db:backup # backup database
424
473
  ```
@@ -0,0 +1,30 @@
1
+ /**
2
+ * date-extractor — Parse Russian natural language date patterns from query strings.
3
+ *
4
+ * Supports:
5
+ * - Exact date: "3 марта 2026" → date_from=2026-03-03, date_to=2026-03-03
6
+ * - Month range: "в феврале–марте 2025" → date_from=2025-02-01, date_to=2025-03-31
7
+ * - Single month: "в мае 2025" → date_from=2025-05-01, date_to=2025-05-31
8
+ *
9
+ * Year-only patterns ("2025 года") intentionally not supported — too aggressive,
10
+ * causes regressions on non-temporal queries that mention years contextually.
11
+ *
12
+ * Returns a cleaned query with all matched date tokens stripped.
13
+ */
14
+ export interface ExtractedDates {
15
+ date_from: string | null;
16
+ date_to: string | null;
17
+ cleanedQuery: string;
18
+ }
19
+ /**
20
+ * Parse Russian date expressions from a natural language query.
21
+ *
22
+ * Patterns (checked in order of specificity):
23
+ * 1. Exact date: <1-31> <month_gen> <year>
24
+ * 2. Month range: <month>[–-]<month> <year>
25
+ * 3. Single month: <month> <year>
26
+ *
27
+ * Matching is case-insensitive; ё is treated as е (same as FTS5 normalization).
28
+ */
29
+ export declare function extractDatesFromQuery(query: string): ExtractedDates;
30
+ //# sourceMappingURL=date-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date-extractor.d.ts","sourceRoot":"","sources":["../src/date-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAiED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAmHnE"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * date-extractor — Parse Russian natural language date patterns from query strings.
3
+ *
4
+ * Supports:
5
+ * - Exact date: "3 марта 2026" → date_from=2026-03-03, date_to=2026-03-03
6
+ * - Month range: "в феврале–марте 2025" → date_from=2025-02-01, date_to=2025-03-31
7
+ * - Single month: "в мае 2025" → date_from=2025-05-01, date_to=2025-05-31
8
+ *
9
+ * Year-only patterns ("2025 года") intentionally not supported — too aggressive,
10
+ * causes regressions on non-temporal queries that mention years contextually.
11
+ *
12
+ * Returns a cleaned query with all matched date tokens stripped.
13
+ */
14
+ /** Month name → month number (1-based). All forms normalized to lowercase. */
15
+ const MONTH_MAP = {
16
+ // January
17
+ январь: 1, января: 1, январе: 1,
18
+ // February
19
+ февраль: 2, февраля: 2, феврале: 2,
20
+ // March
21
+ март: 3, марта: 3, марте: 3,
22
+ // April
23
+ апрель: 4, апреля: 4, апреле: 4,
24
+ // May
25
+ май: 5, мая: 5, мае: 5,
26
+ // June
27
+ июнь: 6, июня: 6, июне: 6,
28
+ // July
29
+ июль: 7, июля: 7, июле: 7,
30
+ // August
31
+ август: 8, августа: 8, августе: 8,
32
+ // September
33
+ сентябрь: 9, сентября: 9, сентябре: 9,
34
+ // October
35
+ октябрь: 10, октября: 10, октябре: 10,
36
+ // November
37
+ ноябрь: 11, ноября: 11, ноябре: 11,
38
+ // December
39
+ декабрь: 12, декабря: 12, декабре: 12,
40
+ };
41
+ /** Return true if year is a leap year. */
42
+ function isLeapYear(year) {
43
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
44
+ }
45
+ /** Return last day of month (1-based month). */
46
+ function lastDayOfMonth(month, year) {
47
+ switch (month) {
48
+ case 1:
49
+ case 3:
50
+ case 5:
51
+ case 7:
52
+ case 8:
53
+ case 10:
54
+ case 12:
55
+ return 31;
56
+ case 4:
57
+ case 6:
58
+ case 9:
59
+ case 11:
60
+ return 30;
61
+ case 2:
62
+ return isLeapYear(year) ? 29 : 28;
63
+ default:
64
+ return 30;
65
+ }
66
+ }
67
+ /** Format date parts to ISO string "YYYY-MM-DD". */
68
+ function toIso(year, month, day) {
69
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
70
+ }
71
+ /**
72
+ * Build regex source string for matching any month name.
73
+ * Sorted by length descending to ensure longer forms match first.
74
+ */
75
+ function buildMonthPattern() {
76
+ const forms = Object.keys(MONTH_MAP).sort((a, b) => b.length - a.length);
77
+ return forms.join("|");
78
+ }
79
+ const MONTH_PATTERN = buildMonthPattern();
80
+ /**
81
+ * Parse Russian date expressions from a natural language query.
82
+ *
83
+ * Patterns (checked in order of specificity):
84
+ * 1. Exact date: <1-31> <month_gen> <year>
85
+ * 2. Month range: <month>[–-]<month> <year>
86
+ * 3. Single month: <month> <year>
87
+ *
88
+ * Matching is case-insensitive; ё is treated as е (same as FTS5 normalization).
89
+ */
90
+ export function extractDatesFromQuery(query) {
91
+ // Normalize ё→е for matching, keep same string length so character offsets stay valid.
92
+ const norm = query.toLowerCase().replace(/ё/g, "е");
93
+ let date_from = null;
94
+ let date_to = null;
95
+ const stripRanges = [];
96
+ let matched = false;
97
+ let m;
98
+ // Pattern 1: Exact date — "3 марта 2026" or "3 марта 2026 года"
99
+ const exactPattern = new RegExp(`(\\d{1,2})\\s+(${MONTH_PATTERN})\\s+(\\d{4})(?:\\s+года?(?:у)?)?`, "gi");
100
+ exactPattern.lastIndex = 0;
101
+ while ((m = exactPattern.exec(norm)) !== null) {
102
+ const day = parseInt(m[1], 10);
103
+ const monthNum = MONTH_MAP[m[2]];
104
+ const year = parseInt(m[3], 10);
105
+ if (monthNum && day >= 1 && day <= 31 && year >= 1900 && year <= 2099) {
106
+ date_from = toIso(year, monthNum, day);
107
+ date_to = toIso(year, monthNum, day);
108
+ stripRanges.push([m.index, m.index + m[0].length]);
109
+ matched = true;
110
+ break;
111
+ }
112
+ }
113
+ if (!matched) {
114
+ // Pattern 2: Month range — "феврале–марте 2025" or "феврале-марте 2025"
115
+ // Dash variants: hyphen (-), en-dash (–, \u2013), em-dash (—, \u2014), figure dash (\u2012)
116
+ const rangePattern = new RegExp(`(${MONTH_PATTERN})\\s*[\\-\\u2012\\u2013\\u2014]\\s*(${MONTH_PATTERN})\\s+(\\d{4})(?:\\s+года?(?:у)?)?`, "gi");
117
+ rangePattern.lastIndex = 0;
118
+ m = rangePattern.exec(norm);
119
+ if (m) {
120
+ const month1 = MONTH_MAP[m[1]];
121
+ const month2 = MONTH_MAP[m[2]];
122
+ const year = parseInt(m[3], 10);
123
+ if (month1 && month2 && year >= 1900 && year <= 2099) {
124
+ const fromMonth = Math.min(month1, month2);
125
+ const toMonth = Math.max(month1, month2);
126
+ date_from = toIso(year, fromMonth, 1);
127
+ date_to = toIso(year, toMonth, lastDayOfMonth(toMonth, year));
128
+ stripRanges.push([m.index, m.index + m[0].length]);
129
+ matched = true;
130
+ }
131
+ }
132
+ }
133
+ if (!matched) {
134
+ // Pattern 3: Single month + year — "в мае 2025"
135
+ const singlePattern = new RegExp(`(${MONTH_PATTERN})\\s+(\\d{4})(?:\\s+года?(?:у)?)?`, "gi");
136
+ singlePattern.lastIndex = 0;
137
+ m = singlePattern.exec(norm);
138
+ if (m) {
139
+ const monthNum = MONTH_MAP[m[1]];
140
+ const year = parseInt(m[2], 10);
141
+ if (monthNum && year >= 1900 && year <= 2099) {
142
+ date_from = toIso(year, monthNum, 1);
143
+ date_to = toIso(year, monthNum, lastDayOfMonth(monthNum, year));
144
+ stripRanges.push([m.index, m.index + m[0].length]);
145
+ matched = true;
146
+ }
147
+ }
148
+ }
149
+ // Pattern 4 (year only) intentionally omitted — too aggressive.
150
+ // "цели на 2025 год" is not a temporal query, it's a topic query mentioning a year.
151
+ // Patterns 1–3 (exact date, month range, single month) are precise enough.
152
+ if (!matched) {
153
+ return { date_from: null, date_to: null, cleanedQuery: query };
154
+ }
155
+ // Strip matched date tokens from the original query.
156
+ // Before stripping, expand each range to absorb a leading "в/во " preposition
157
+ // directly before the date token (common in Russian: "в феврале–марте 2025").
158
+ // Use the normalized string to detect the preposition, but strip from original.
159
+ const expandedRanges = stripRanges.map(([start, end]) => {
160
+ // Look backward in norm for optional whitespace + "в" or "во" + whitespace
161
+ const prefix = norm.slice(0, start);
162
+ const prepMatch = /(?:^|(?<=\s))во?\s+$/.exec(prefix);
163
+ if (prepMatch) {
164
+ return [start - prepMatch[0].length, end];
165
+ }
166
+ return [start, end];
167
+ });
168
+ // Apply in reverse order so earlier offsets stay valid
169
+ let cleaned = query;
170
+ const sorted = expandedRanges.slice().sort((a, b) => b[0] - a[0]);
171
+ for (const [start, end] of sorted) {
172
+ cleaned = cleaned.slice(0, start) + " " + cleaned.slice(end);
173
+ }
174
+ // Word boundary pattern for Cyrillic + ASCII (JS \b doesn't work with Cyrillic)
175
+ const wb = "(?<![а-яёА-ЯЁa-zA-Z\\d])";
176
+ const we = "(?![а-яёА-ЯЁa-zA-Z\\d])";
177
+ // Strip residual year words not captured inside the pattern match
178
+ cleaned = cleaned.replace(new RegExp(`${wb}года?${we}`, "gi"), " ");
179
+ cleaned = cleaned.replace(new RegExp(`${wb}году${we}`, "gi"), " ");
180
+ // Collapse whitespace and trim surrounding punctuation
181
+ cleaned = cleaned.replace(/\s{2,}/g, " ").trim();
182
+ cleaned = cleaned.replace(/^[?!.,;:\s—–\-]+|[?!.,;:\s—–\-]+$/g, "").trim();
183
+ return { date_from, date_to, cleanedQuery: cleaned };
184
+ }
185
+ //# sourceMappingURL=date-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date-extractor.js","sourceRoot":"","sources":["../src/date-extractor.ts"],"names":[],"mappings":"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"}
package/dist/db.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAOtC,QAAA,MAAM,OAAO,QAA6D,CAAC;AAK3E;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,MAAgB,GAAG,QAAQ,CAAC,QAAQ,CAgBxE;AAiWD,OAAO,EAAE,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAOtC,QAAA,MAAM,OAAO,QAA6D,CAAC;AAK3E;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,MAAgB,GAAG,QAAQ,CAAC,QAAQ,CAgBxE;AAkZD,OAAO,EAAE,OAAO,EAAE,CAAC"}